@objectstack/runtime 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -32,13 +32,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
33
 
34
34
  // src/load-artifact-bundle.ts
35
- var load_artifact_bundle_exports = {};
36
- __export(load_artifact_bundle_exports, {
37
- isHttpUrl: () => isHttpUrl,
38
- loadArtifactBundle: () => loadArtifactBundle,
39
- mergeRuntimeModule: () => mergeRuntimeModule,
40
- readArtifactSource: () => readArtifactSource
41
- });
42
35
  function isHttpUrl(pathOrUrl) {
43
36
  return /^https?:\/\//i.test(pathOrUrl);
44
37
  }
@@ -294,17 +287,37 @@ var init_seed_loader = __esm({
294
287
  }
295
288
  const objectRefs = refMap.get(objectName) || [];
296
289
  const seedNow = /* @__PURE__ */ new Date();
290
+ const seedIdentity = config.identity;
291
+ const baseEvalCtx = {
292
+ now: seedNow,
293
+ user: seedIdentity?.user,
294
+ // Fall back to the per-tenant organizationId so `os.org.id` resolves
295
+ // during per-org replay even without an explicit identity.org.
296
+ org: seedIdentity?.org ?? (config.organizationId ? { id: config.organizationId } : void 0),
297
+ env: config.env
298
+ };
297
299
  for (let i = 0; i < dataset.records.length; i++) {
298
300
  const seedResult = (0, import_formula.resolveSeedRecord)(
299
301
  dataset.records[i],
300
- { now: seedNow }
302
+ baseEvalCtx
301
303
  );
302
- const record = seedResult.ok ? { ...seedResult.value } : { ...dataset.records[i] };
303
304
  if (!seedResult.ok) {
304
- this.logger.warn(
305
- `[SeedLoader] Failed to resolve dynamic values for ${objectName} record #${i}: ${seedResult.error.message}`
306
- );
305
+ errored++;
306
+ const error = {
307
+ sourceObject: objectName,
308
+ field: "(expression)",
309
+ targetObject: objectName,
310
+ targetField: "(expression)",
311
+ attemptedValue: dataset.records[i],
312
+ recordIndex: i,
313
+ message: `Cannot resolve dynamic seed values for ${objectName} record #${i}: ${seedResult.error.message}. Records using cel\`os.user.id\` / cel\`os.org.id\` require a seed identity \u2014 ensure a system/admin user exists before seeding (see SeedLoaderConfig.identity).`
314
+ };
315
+ errors.push(error);
316
+ allErrors.push(error);
317
+ this.logger.warn(`[SeedLoader] ${error.message}`);
318
+ continue;
307
319
  }
320
+ const record = { ...seedResult.value };
308
321
  if (config.organizationId && record["organization_id"] == null) {
309
322
  record["organization_id"] = config.organizationId;
310
323
  }
@@ -383,10 +396,18 @@ var init_seed_loader = __esm({
383
396
  }
384
397
  } catch (err) {
385
398
  errored++;
386
- this.logger.warn(`[SeedLoader] Failed to write ${objectName} record`, {
387
- error: err.message,
388
- recordIndex: i
389
- });
399
+ const error = {
400
+ sourceObject: objectName,
401
+ field: "(write)",
402
+ targetObject: objectName,
403
+ targetField: externalId,
404
+ attemptedValue: record[externalId] ?? null,
405
+ recordIndex: i,
406
+ message: `Failed to write ${objectName} record #${i} (${externalId}=${String(record[externalId] ?? "")}): ${err.message}`
407
+ };
408
+ errors.push(error);
409
+ allErrors.push(error);
410
+ this.logger.warn(`[SeedLoader] ${error.message}`, { recordIndex: i });
390
411
  }
391
412
  } else {
392
413
  const externalIdValue = String(record[externalId] ?? "");
@@ -1330,13 +1351,14 @@ function collectBundleFunctions(bundle) {
1330
1351
  merge(bundle?.manifest?.functions);
1331
1352
  return out;
1332
1353
  }
1333
- var import_types, AppPlugin;
1354
+ var import_types, import_system, AppPlugin;
1334
1355
  var init_app_plugin = __esm({
1335
1356
  "src/app-plugin.ts"() {
1336
1357
  "use strict";
1337
1358
  import_types = require("@objectstack/types");
1338
1359
  init_seed_loader();
1339
1360
  init_package_state_store();
1361
+ import_system = require("@objectstack/spec/system");
1340
1362
  init_quickjs_runner();
1341
1363
  init_body_runner();
1342
1364
  AppPlugin = class {
@@ -1411,6 +1433,27 @@ var init_app_plugin = __esm({
1411
1433
  });
1412
1434
  ql.setDatasourceMapping(this.bundle.datasourceMapping);
1413
1435
  }
1436
+ try {
1437
+ const dsDefs = this.bundle.datasources;
1438
+ const dsList = Array.isArray(dsDefs) ? dsDefs : dsDefs && typeof dsDefs === "object" ? Object.entries(dsDefs).map(([name, def]) => ({ name, ...def })) : [];
1439
+ if (dsList.length > 0) {
1440
+ const metadata = ctx.getService("metadata");
1441
+ if (typeof metadata?.registerInMemory === "function") {
1442
+ for (const ds of dsList) {
1443
+ if (!ds?.name) continue;
1444
+ metadata.registerInMemory("datasource", ds.name, { ...ds, origin: "code" });
1445
+ }
1446
+ ctx.logger.info("Registered code-defined datasources in metadata registry", {
1447
+ appId,
1448
+ count: dsList.length
1449
+ });
1450
+ }
1451
+ }
1452
+ } catch (err) {
1453
+ ctx.logger.warn("[AppPlugin] failed to register code-defined datasources", {
1454
+ error: err?.message ?? String(err)
1455
+ });
1456
+ }
1414
1457
  const stackBundle = this.bundle.default || this.bundle;
1415
1458
  const runtime = stackBundle && typeof stackBundle.onEnable === "function" ? stackBundle : this.bundle;
1416
1459
  if (runtime && typeof runtime.onEnable === "function") {
@@ -1505,49 +1548,6 @@ var init_app_plugin = __esm({
1505
1548
  appId
1506
1549
  });
1507
1550
  }
1508
- try {
1509
- const approvals = Array.isArray(this.bundle.approvals) ? this.bundle.approvals : Array.isArray((this.bundle.manifest || {}).approvals) ? this.bundle.manifest.approvals : [];
1510
- if (approvals.length > 0) {
1511
- ctx.hook("kernel:ready", async () => {
1512
- let svc;
1513
- try {
1514
- svc = ctx.getService("approvals");
1515
- } catch {
1516
- }
1517
- if (!svc || typeof svc.defineProcess !== "function") {
1518
- ctx.logger.warn("[AppPlugin] approvals service not registered \u2014 skipping declarative processes", {
1519
- appId,
1520
- processCount: approvals.length
1521
- });
1522
- return;
1523
- }
1524
- const sysCtx = { isSystem: true, roles: [], permissions: [] };
1525
- let ok = 0;
1526
- for (const proc of approvals) {
1527
- try {
1528
- await svc.defineProcess({
1529
- name: proc.name,
1530
- label: proc.label,
1531
- object: proc.object,
1532
- description: proc.description,
1533
- active: proc.active !== false,
1534
- definition: proc
1535
- }, sysCtx);
1536
- ok++;
1537
- } catch (err) {
1538
- ctx.logger.warn("[AppPlugin] Failed to register approval process", {
1539
- appId,
1540
- process: proc?.name,
1541
- error: err?.message ?? String(err)
1542
- });
1543
- }
1544
- }
1545
- ctx.logger.info("[AppPlugin] Registered approval processes", { appId, count: ok });
1546
- });
1547
- }
1548
- } catch (err) {
1549
- ctx.logger.error("[AppPlugin] Failed to schedule approval-process registration", err, { appId });
1550
- }
1551
1551
  try {
1552
1552
  const jobs = Array.isArray(this.bundle.jobs) ? this.bundle.jobs : Array.isArray((this.bundle.manifest || {}).jobs) ? this.bundle.manifest.jobs : [];
1553
1553
  if (jobs.length > 0) {
@@ -1624,6 +1624,7 @@ var init_app_plugin = __esm({
1624
1624
  ...d,
1625
1625
  object: d.object
1626
1626
  }));
1627
+ const seedIdentity = await this.ensureSeedIdentity(ql, ctx.logger);
1627
1628
  try {
1628
1629
  const kernel = ctx.kernel;
1629
1630
  const existing = (() => {
@@ -1665,7 +1666,12 @@ var init_app_plugin = __esm({
1665
1666
  config: {
1666
1667
  defaultMode: "upsert",
1667
1668
  multiPass: true,
1668
- organizationId
1669
+ organizationId,
1670
+ // Bind os.user (system identity) and os.org (this
1671
+ // tenant) so identity-derived seed values resolve
1672
+ // per-org. org.id falls back to organizationId
1673
+ // inside the loader when identity.org is absent.
1674
+ identity: seedIdentity
1669
1675
  }
1670
1676
  });
1671
1677
  const result = await seedLoader.load(request);
@@ -1693,14 +1699,34 @@ var init_app_plugin = __esm({
1693
1699
  const { SeedLoaderRequestSchema } = await import("@objectstack/spec/data");
1694
1700
  const request = SeedLoaderRequestSchema.parse({
1695
1701
  datasets: normalizedDatasets,
1696
- config: { defaultMode: "upsert", multiPass: true }
1702
+ config: { defaultMode: "upsert", multiPass: true, identity: seedIdentity }
1697
1703
  });
1698
1704
  const result = await seedLoader.load(request);
1699
- ctx.logger.info("[Seeder] Seed loading complete", {
1700
- inserted: result.summary.totalInserted,
1701
- updated: result.summary.totalUpdated,
1702
- errors: result.errors.length
1703
- });
1705
+ const { totalInserted, totalUpdated, totalSkipped, totalErrored } = result.summary;
1706
+ if (result.success) {
1707
+ ctx.logger.info("[Seeder] Seed loading complete", {
1708
+ inserted: totalInserted,
1709
+ updated: totalUpdated,
1710
+ skipped: totalSkipped,
1711
+ errored: totalErrored
1712
+ });
1713
+ } else {
1714
+ ctx.logger.warn(
1715
+ `[Seeder] Seed loading completed with ${totalErrored} dropped record(s) and ${result.errors.length} error(s) for ${appId}`,
1716
+ {
1717
+ inserted: totalInserted,
1718
+ updated: totalUpdated,
1719
+ skipped: totalSkipped,
1720
+ errored: totalErrored
1721
+ }
1722
+ );
1723
+ for (const e of result.errors.slice(0, 20)) {
1724
+ ctx.logger.warn(`[Seeder] \u2717 ${e.message}`);
1725
+ }
1726
+ if (result.errors.length > 20) {
1727
+ ctx.logger.warn(`[Seeder] \u2026and ${result.errors.length - 20} more error(s)`);
1728
+ }
1729
+ }
1704
1730
  } else {
1705
1731
  ctx.logger.debug("[Seeder] No metadata service; using basic insert fallback");
1706
1732
  for (const dataset of normalizedDatasets) {
@@ -1802,6 +1828,64 @@ var init_app_plugin = __esm({
1802
1828
  this.name = `plugin.app.${appId}`;
1803
1829
  this.version = sys?.version;
1804
1830
  }
1831
+ /**
1832
+ * Resolve the identity bound to `os.user` / `os.org` for seed CEL values.
1833
+ *
1834
+ * On a fresh boot there are zero users until the first human sign-up
1835
+ * (which the SeedLoader runs *before*), so identity-derived seeds like
1836
+ * `owner_id: cel`os.user.id`` had nothing to resolve against and were
1837
+ * dropped silently. To make seeds deterministic and self-sufficient we
1838
+ * upsert a single non-loginable **system user** (`usr_system`) and bind
1839
+ * it as `os.user`.
1840
+ *
1841
+ * Why a dedicated system user rather than the login admin:
1842
+ * - `sys_user` is better-auth-managed and schema-locked (ADR-0010); the
1843
+ * password lives in `sys_account`, so a *loginable* admin can only be
1844
+ * minted through better-auth (the CLI does this via HTTP sign-up after
1845
+ * boot). A raw insert here would bypass those invariants.
1846
+ * - `usr_system` is an owner identity only (no credential row), analogous
1847
+ * to Salesforce's "Automated Process" user. The human admin is created
1848
+ * independently and need not be the seed owner.
1849
+ *
1850
+ * Idempotent: matches by the stable id, inserts once, reuses thereafter.
1851
+ * Failures are non-fatal (logged) — records that actually need `os.user`
1852
+ * then fail loudly in the loader with an actionable message.
1853
+ */
1854
+ async ensureSeedIdentity(ql, logger) {
1855
+ const SYSTEM_USER_ID = import_system.SystemUserId.SYSTEM;
1856
+ const SYSTEM_USER_EMAIL = "system@objectstack.local";
1857
+ const identity = { user: { id: SYSTEM_USER_ID, role: "system", email: SYSTEM_USER_EMAIL } };
1858
+ const opts = { context: { isSystem: true } };
1859
+ try {
1860
+ const existing = await ql.find(
1861
+ "sys_user",
1862
+ { where: { id: SYSTEM_USER_ID }, limit: 1 },
1863
+ opts
1864
+ );
1865
+ if (Array.isArray(existing) && existing.length > 0) {
1866
+ return identity;
1867
+ }
1868
+ await ql.insert(
1869
+ "sys_user",
1870
+ {
1871
+ id: SYSTEM_USER_ID,
1872
+ name: "System",
1873
+ email: SYSTEM_USER_EMAIL,
1874
+ email_verified: true,
1875
+ role: "system"
1876
+ },
1877
+ opts
1878
+ );
1879
+ logger.info(
1880
+ `[Seeder] Provisioned deterministic system user (${SYSTEM_USER_ID}) as seed owner \u2014 binds os.user for identity-derived seed values`
1881
+ );
1882
+ } catch (err) {
1883
+ logger.warn("[Seeder] Failed to ensure system seed user; os.user-dependent seeds may be dropped", {
1884
+ error: err?.message ?? String(err)
1885
+ });
1886
+ }
1887
+ return identity;
1888
+ }
1805
1889
  /**
1806
1890
  * Emit a kernel hook so the control-plane `AppCatalogService` can
1807
1891
  * upsert / delete the corresponding `sys_app` row. Silently no-ops
@@ -2088,294 +2172,88 @@ var init_standalone_stack = __esm({
2088
2172
  }
2089
2173
  });
2090
2174
 
2091
- // src/cloud/platform-sso.ts
2092
- var platform_sso_exports = {};
2093
- __export(platform_sso_exports, {
2094
- PLATFORM_SSO_PROVIDER_ID: () => PLATFORM_SSO_PROVIDER_ID,
2095
- backfillPlatformSsoClients: () => backfillPlatformSsoClients,
2096
- buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
2097
- derivePlatformSsoClientId: () => derivePlatformSsoClientId,
2098
- derivePlatformSsoClientSecret: () => derivePlatformSsoClientSecret,
2099
- hashPlatformSsoClientSecret: () => hashPlatformSsoClientSecret,
2100
- seedPlatformSsoClient: () => seedPlatformSsoClient
2175
+ // src/cloud/environment-org-seed.ts
2176
+ var environment_org_seed_exports = {};
2177
+ __export(environment_org_seed_exports, {
2178
+ seedProjectMember: () => seedProjectMember,
2179
+ seedProjectOrganization: () => seedProjectOrganization
2101
2180
  });
2102
- function derivePlatformSsoClientId(environmentId) {
2103
- return `project_${environmentId}`;
2104
- }
2105
- function derivePlatformSsoClientSecret(baseSecret, environmentId) {
2106
- return (0, import_node_crypto.createHmac)("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
2107
- }
2108
- function hashPlatformSsoClientSecret(plaintext) {
2109
- return (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
2110
- }
2111
- function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
2112
- let host;
2113
- if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
2114
- host = hostname;
2115
- } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
2116
- const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
2117
- const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
2118
- host = `http://${hostWithPort}`;
2119
- } else {
2120
- host = `https://${hostname}`;
2121
- }
2122
- const trimmed = host.replace(/\/+$/, "");
2123
- const path = basePath.replace(/\/+$/, "");
2124
- return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
2125
- }
2126
- async function seedPlatformSsoClient(opts) {
2127
- const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
2128
- if (!baseSecret) {
2129
- logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
2130
- return;
2131
- }
2132
- const clientId = derivePlatformSsoClientId(environmentId);
2133
- const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
2134
- const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
2135
- const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
2136
- let existing = null;
2181
+ async function seedProjectOrganization(kernel, seed, logger) {
2182
+ if (!seed?.id || !seed?.name) return "skipped";
2137
2183
  try {
2138
- const rows = await ql.find("sys_oauth_application", {
2139
- where: { client_id: clientId },
2140
- limit: 1
2141
- }, { context: { isSystem: true } });
2142
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
2143
- existing = list[0] ?? null;
2184
+ const ql = kernel.getService("objectql");
2185
+ if (!ql?.insert || !ql?.find) {
2186
+ logger?.warn?.("[seedProjectOrganization] objectql service unavailable", { orgId: seed.id });
2187
+ return "skipped";
2188
+ }
2189
+ try {
2190
+ const existing = await ql.find(SYS_ORG, { where: { id: seed.id } });
2191
+ const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2192
+ if (Array.isArray(rows) && rows.length > 0) return "exists";
2193
+ } catch {
2194
+ }
2195
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2196
+ await ql.insert(SYS_ORG, {
2197
+ id: seed.id,
2198
+ name: seed.name,
2199
+ slug: seed.slug ?? null,
2200
+ logo: seed.logo ?? null,
2201
+ metadata: null,
2202
+ created_at: nowIso
2203
+ });
2204
+ logger?.info?.("[seedProjectOrganization] org seeded", {
2205
+ orgId: seed.id,
2206
+ name: seed.name
2207
+ });
2208
+ return "inserted";
2144
2209
  } catch (err) {
2145
- logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
2146
- environmentId,
2210
+ logger?.warn?.("[seedProjectOrganization] failed (non-fatal)", {
2211
+ orgId: seed.id,
2147
2212
  error: err?.message
2148
2213
  });
2149
- return;
2214
+ return "error";
2150
2215
  }
2151
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2152
- if (!existing) {
2153
- const redirects = desiredRedirect ? [desiredRedirect] : [];
2216
+ }
2217
+ async function seedProjectMember(kernel, args, logger) {
2218
+ const { userId, organizationId } = args;
2219
+ const role = args.role ?? "member";
2220
+ if (!userId || !organizationId) return "skipped";
2221
+ try {
2222
+ const ql = kernel.getService("objectql");
2223
+ if (!ql?.insert || !ql?.find) {
2224
+ logger?.warn?.("[seedProjectMember] objectql service unavailable", { userId, organizationId });
2225
+ return "skipped";
2226
+ }
2154
2227
  try {
2155
- await ql.insert("sys_oauth_application", {
2156
- id: `oauthc_${environmentId}`,
2157
- name: `Project ${environmentId}`,
2158
- client_id: clientId,
2159
- client_secret: clientSecretStored,
2160
- type: "web",
2161
- redirect_uris: JSON.stringify(redirects),
2162
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
2163
- response_types: JSON.stringify(["code"]),
2164
- scopes: JSON.stringify(["openid", "email", "profile"]),
2165
- token_endpoint_auth_method: "client_secret_basic",
2166
- require_pkce: false,
2167
- skip_consent: true,
2168
- disabled: false,
2169
- subject_type: "public",
2170
- created_at: nowIso,
2171
- updated_at: nowIso
2172
- }, { context: { isSystem: true } });
2173
- logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
2174
- } catch (err) {
2175
- logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
2176
- environmentId,
2177
- error: err?.message
2228
+ const existing = await ql.find("sys_member", {
2229
+ where: { user_id: userId, organization_id: organizationId }
2178
2230
  });
2179
- if (throwOnError) throw err;
2231
+ const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2232
+ if (Array.isArray(rows) && rows.length > 0) return "exists";
2233
+ } catch {
2180
2234
  }
2181
- return;
2182
- }
2183
- let currentRedirects = [];
2184
- try {
2185
- const raw = existing.redirect_uris;
2186
- const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
2187
- if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
2188
- } catch {
2189
- }
2190
- const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
2191
- const repairPatch = {
2192
- name: existing.name || `Project ${environmentId}`,
2193
- client_secret: clientSecretStored,
2194
- type: existing.type || "web",
2195
- redirect_uris: JSON.stringify(mergedRedirects),
2196
- grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
2197
- response_types: JSON.stringify(["code"]),
2198
- scopes: JSON.stringify(["openid", "email", "profile"]),
2199
- token_endpoint_auth_method: "client_secret_basic",
2200
- require_pkce: false,
2201
- skip_consent: true,
2202
- disabled: false,
2203
- subject_type: "public",
2204
- updated_at: nowIso
2205
- };
2206
- try {
2207
- await ql.update(
2208
- "sys_oauth_application",
2209
- repairPatch,
2210
- { where: { id: existing.id } },
2211
- { context: { isSystem: true } }
2212
- );
2213
- logger?.info?.("[platform-sso] sys_oauth_application repaired", {
2214
- environmentId,
2215
- clientId,
2216
- redirect_uris: mergedRedirects
2235
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2236
+ const memId = `mem_${Math.random().toString(36).slice(2, 14)}`;
2237
+ await ql.insert("sys_member", {
2238
+ id: memId,
2239
+ organization_id: organizationId,
2240
+ user_id: userId,
2241
+ role,
2242
+ created_at: nowIso
2243
+ });
2244
+ logger?.info?.("[seedProjectMember] member seeded", {
2245
+ userId,
2246
+ organizationId,
2247
+ role
2217
2248
  });
2249
+ return "inserted";
2218
2250
  } catch (err) {
2219
- logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
2220
- environmentId,
2251
+ logger?.warn?.("[seedProjectMember] failed (non-fatal)", {
2252
+ userId,
2253
+ organizationId,
2221
2254
  error: err?.message
2222
2255
  });
2223
- if (throwOnError) throw err;
2224
- }
2225
- }
2226
- async function backfillPlatformSsoClients(opts) {
2227
- const { ql, baseSecret, logger, limit = 1e3 } = opts;
2228
- if (!baseSecret) {
2229
- logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
2230
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
2231
- }
2232
- let projects = [];
2233
- try {
2234
- const rows = await ql.find("sys_environment", {
2235
- limit,
2236
- fields: ["id", "hostname", "status"]
2237
- }, { context: { isSystem: true } });
2238
- projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
2239
- } catch (err) {
2240
- logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
2241
- error: err?.message
2242
- });
2243
- return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
2244
- }
2245
- let seeded = 0;
2246
- let alreadyExisted = 0;
2247
- const failures = [];
2248
- for (const p of projects) {
2249
- if (!p?.id) continue;
2250
- const before = await (async () => {
2251
- try {
2252
- const r = await ql.find("sys_oauth_application", {
2253
- where: { client_id: derivePlatformSsoClientId(p.id) },
2254
- limit: 1
2255
- }, { context: { isSystem: true } });
2256
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2257
- return list[0] ?? null;
2258
- } catch {
2259
- return null;
2260
- }
2261
- })();
2262
- try {
2263
- await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
2264
- if (before) alreadyExisted++;
2265
- else {
2266
- const after = await (async () => {
2267
- try {
2268
- const r = await ql.find("sys_oauth_application", {
2269
- where: { client_id: derivePlatformSsoClientId(p.id) },
2270
- limit: 1
2271
- }, { context: { isSystem: true } });
2272
- const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
2273
- return list[0] ?? null;
2274
- } catch (err) {
2275
- return { _readErr: err?.message };
2276
- }
2277
- })();
2278
- if (after && !after._readErr) seeded++;
2279
- else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
2280
- }
2281
- } catch (err) {
2282
- failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
2283
- }
2284
- }
2285
- logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
2286
- return { scanned: projects.length, seeded, alreadyExisted, failures };
2287
- }
2288
- var import_node_crypto, PLATFORM_SSO_PROVIDER_ID;
2289
- var init_platform_sso = __esm({
2290
- "src/cloud/platform-sso.ts"() {
2291
- "use strict";
2292
- import_node_crypto = require("crypto");
2293
- PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
2294
- }
2295
- });
2296
-
2297
- // src/cloud/environment-org-seed.ts
2298
- var environment_org_seed_exports = {};
2299
- __export(environment_org_seed_exports, {
2300
- seedProjectMember: () => seedProjectMember,
2301
- seedProjectOrganization: () => seedProjectOrganization
2302
- });
2303
- async function seedProjectOrganization(kernel, seed, logger) {
2304
- if (!seed?.id || !seed?.name) return "skipped";
2305
- try {
2306
- const ql = kernel.getService("objectql");
2307
- if (!ql?.insert || !ql?.find) {
2308
- logger?.warn?.("[seedProjectOrganization] objectql service unavailable", { orgId: seed.id });
2309
- return "skipped";
2310
- }
2311
- try {
2312
- const existing = await ql.find(SYS_ORG, { where: { id: seed.id } });
2313
- const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2314
- if (Array.isArray(rows) && rows.length > 0) return "exists";
2315
- } catch {
2316
- }
2317
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2318
- await ql.insert(SYS_ORG, {
2319
- id: seed.id,
2320
- name: seed.name,
2321
- slug: seed.slug ?? null,
2322
- logo: seed.logo ?? null,
2323
- metadata: null,
2324
- created_at: nowIso
2325
- });
2326
- logger?.info?.("[seedProjectOrganization] org seeded", {
2327
- orgId: seed.id,
2328
- name: seed.name
2329
- });
2330
- return "inserted";
2331
- } catch (err) {
2332
- logger?.warn?.("[seedProjectOrganization] failed (non-fatal)", {
2333
- orgId: seed.id,
2334
- error: err?.message
2335
- });
2336
- return "error";
2337
- }
2338
- }
2339
- async function seedProjectMember(kernel, args, logger) {
2340
- const { userId, organizationId } = args;
2341
- const role = args.role ?? "member";
2342
- if (!userId || !organizationId) return "skipped";
2343
- try {
2344
- const ql = kernel.getService("objectql");
2345
- if (!ql?.insert || !ql?.find) {
2346
- logger?.warn?.("[seedProjectMember] objectql service unavailable", { userId, organizationId });
2347
- return "skipped";
2348
- }
2349
- try {
2350
- const existing = await ql.find("sys_member", {
2351
- where: { user_id: userId, organization_id: organizationId }
2352
- });
2353
- const rows = Array.isArray(existing) ? existing : existing?.value ?? [];
2354
- if (Array.isArray(rows) && rows.length > 0) return "exists";
2355
- } catch {
2356
- }
2357
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2358
- const memId = `mem_${Math.random().toString(36).slice(2, 14)}`;
2359
- await ql.insert("sys_member", {
2360
- id: memId,
2361
- organization_id: organizationId,
2362
- user_id: userId,
2363
- role,
2364
- created_at: nowIso
2365
- });
2366
- logger?.info?.("[seedProjectMember] member seeded", {
2367
- userId,
2368
- organizationId,
2369
- role
2370
- });
2371
- return "inserted";
2372
- } catch (err) {
2373
- logger?.warn?.("[seedProjectMember] failed (non-fatal)", {
2374
- userId,
2375
- organizationId,
2376
- error: err?.message
2377
- });
2378
- return "error";
2256
+ return "error";
2379
2257
  }
2380
2258
  }
2381
2259
  var SYS_ORG;
@@ -2451,6 +2329,7 @@ __export(index_exports, {
2451
2329
  DEFAULT_CLOUD_URL: () => DEFAULT_CLOUD_URL,
2452
2330
  DEFAULT_RATE_LIMITS: () => DEFAULT_RATE_LIMITS,
2453
2331
  DriverPlugin: () => DriverPlugin,
2332
+ ExternalValidationPlugin: () => ExternalValidationPlugin,
2454
2333
  FileArtifactApiClient: () => FileArtifactApiClient,
2455
2334
  HttpDispatcher: () => HttpDispatcher,
2456
2335
  HttpServer: () => HttpServer,
@@ -2479,7 +2358,7 @@ __export(index_exports, {
2479
2358
  SandboxError: () => SandboxError,
2480
2359
  SeedLoaderService: () => SeedLoaderService,
2481
2360
  UnimplementedScriptRunner: () => UnimplementedScriptRunner,
2482
- _resetEnvDeprecationWarnings: () => import_types6._resetEnvDeprecationWarnings,
2361
+ _resetEnvDeprecationWarnings: () => import_types5._resetEnvDeprecationWarnings,
2483
2362
  actionBodyRunnerFactory: () => actionBodyRunnerFactory,
2484
2363
  backfillPlatformSsoClients: () => backfillPlatformSsoClients,
2485
2364
  buildPlatformSsoRedirectUri: () => buildPlatformSsoRedirectUri,
@@ -2489,6 +2368,7 @@ __export(index_exports, {
2489
2368
  collectBundleHooks: () => collectBundleHooks,
2490
2369
  createDefaultHostConfig: () => createDefaultHostConfig,
2491
2370
  createDispatcherPlugin: () => createDispatcherPlugin,
2371
+ createExternalValidationPlugin: () => createExternalValidationPlugin,
2492
2372
  createObjectOSStack: () => createObjectOSStack,
2493
2373
  createRestApiPlugin: () => import_rest.createRestApiPlugin,
2494
2374
  createStandaloneStack: () => createStandaloneStack,
@@ -2504,7 +2384,7 @@ __export(index_exports, {
2504
2384
  mergeRuntimeModule: () => mergeRuntimeModule,
2505
2385
  parseTraceparent: () => parseTraceparent,
2506
2386
  readArtifactSource: () => readArtifactSource,
2507
- readEnvWithDeprecation: () => import_types6.readEnvWithDeprecation,
2387
+ readEnvWithDeprecation: () => import_types5.readEnvWithDeprecation,
2508
2388
  resolveCloudUrl: () => resolveCloudUrl,
2509
2389
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
2510
2390
  resolveErrorReporter: () => resolveErrorReporter,
@@ -2625,11 +2505,170 @@ init_driver_plugin();
2625
2505
  init_app_plugin();
2626
2506
  init_seed_loader();
2627
2507
 
2508
+ // src/external-validation-plugin.ts
2509
+ var import_shared = require("@objectstack/spec/shared");
2510
+ var ExternalValidationPlugin = class {
2511
+ constructor() {
2512
+ this.name = "com.objectstack.external-validation";
2513
+ this.type = "standard";
2514
+ this.version = "1.0.0";
2515
+ /** Active background drift-check timers, keyed by datasource name. */
2516
+ this.driftTimers = /* @__PURE__ */ new Map();
2517
+ this.init = (_ctx) => {
2518
+ };
2519
+ this.start = (ctx) => {
2520
+ ctx.hook("kernel:ready", async () => {
2521
+ await this.runValidation(ctx);
2522
+ await this.scheduleDriftChecks(ctx);
2523
+ });
2524
+ };
2525
+ /** Tear down background drift-check timers (idempotent). */
2526
+ this.stop = () => {
2527
+ for (const timer of this.driftTimers.values()) clearInterval(timer);
2528
+ this.driftTimers.clear();
2529
+ };
2530
+ }
2531
+ /** Exposed for testing; invoked from the kernel:ready handler. */
2532
+ async runValidation(ctx) {
2533
+ const svc = safeGet(ctx, "external-datasource");
2534
+ if (!svc?.validateAll) {
2535
+ ctx.logger?.debug?.("[external-validation] service not registered; skipping");
2536
+ return;
2537
+ }
2538
+ const metadata = safeGet(ctx, "metadata");
2539
+ let report;
2540
+ try {
2541
+ report = await svc.validateAll();
2542
+ } catch (err) {
2543
+ ctx.logger?.warn?.("[external-validation] validateAll failed", { err });
2544
+ return;
2545
+ }
2546
+ const failures = report.results.filter((r) => !r.ok);
2547
+ if (failures.length === 0) {
2548
+ ctx.logger?.info?.("[external-validation] all federated objects match their remote schema", {
2549
+ objects: report.results.length
2550
+ });
2551
+ return;
2552
+ }
2553
+ for (const r of failures) {
2554
+ const mode = await resolveOnMismatch(metadata, r.datasource);
2555
+ if (mode === "ignore") continue;
2556
+ if (mode === "warn") {
2557
+ ctx.logger?.warn?.("[external-validation] external schema drift", {
2558
+ datasource: r.datasource,
2559
+ object: r.object,
2560
+ diffs: r.diffs
2561
+ });
2562
+ continue;
2563
+ }
2564
+ throw new import_shared.ExternalSchemaMismatchError(r.datasource, r.object, r.diffs);
2565
+ }
2566
+ }
2567
+ /**
2568
+ * Arm a background drift checker for every federated datasource that declares
2569
+ * `external.validation.checkIntervalMs`. Each fires on its own interval and
2570
+ * emits `external.schema.drift` events — it never throws or aborts the
2571
+ * process, since drift past boot is observational, not fatal.
2572
+ *
2573
+ * No-op when metadata can't be enumerated or no datasource opts in. Re-arming
2574
+ * (e.g. a second `kernel:ready`) first clears existing timers so intervals
2575
+ * don't accumulate.
2576
+ */
2577
+ async scheduleDriftChecks(ctx) {
2578
+ this.stop();
2579
+ const metadata = safeGet(ctx, "metadata");
2580
+ if (!metadata?.list) return;
2581
+ let datasources;
2582
+ try {
2583
+ datasources = await metadata.list("datasource");
2584
+ } catch (err) {
2585
+ ctx.logger?.warn?.("[external-validation] could not list datasources for drift checks", { err });
2586
+ return;
2587
+ }
2588
+ for (const def of datasources) {
2589
+ const interval = def?.external?.validation?.checkIntervalMs;
2590
+ const name = def?.name;
2591
+ if (!name || typeof interval !== "number" || interval <= 0) continue;
2592
+ const timer = setInterval(() => {
2593
+ void this.runDriftCheck(ctx, name);
2594
+ }, interval);
2595
+ timer.unref?.();
2596
+ this.driftTimers.set(name, timer);
2597
+ ctx.logger?.info?.("[external-validation] armed background drift check", {
2598
+ datasource: name,
2599
+ intervalMs: interval
2600
+ });
2601
+ }
2602
+ }
2603
+ /**
2604
+ * Re-validate one datasource's federated objects and emit an
2605
+ * `external.schema.drift` event per mismatch. Exposed for testing; invoked
2606
+ * from the interval armed by {@link scheduleDriftChecks}. Never throws.
2607
+ *
2608
+ * @returns the number of drift events emitted.
2609
+ */
2610
+ async runDriftCheck(ctx, datasource) {
2611
+ const svc = safeGet(ctx, "external-datasource");
2612
+ if (!svc?.validateAll) return 0;
2613
+ let report;
2614
+ try {
2615
+ report = await svc.validateAll();
2616
+ } catch (err) {
2617
+ ctx.logger?.warn?.("[external-validation] drift check validateAll failed", {
2618
+ datasource,
2619
+ err
2620
+ });
2621
+ return 0;
2622
+ }
2623
+ const drifted = report.results.filter((r) => !r.ok && r.datasource === datasource);
2624
+ for (const r of drifted) {
2625
+ const event = {
2626
+ datasource: r.datasource,
2627
+ object: r.object,
2628
+ diffs: r.diffs
2629
+ };
2630
+ try {
2631
+ await ctx.trigger("external.schema.drift", event);
2632
+ } catch (err) {
2633
+ ctx.logger?.warn?.("[external-validation] failed to emit drift event", {
2634
+ datasource,
2635
+ object: r.object,
2636
+ err
2637
+ });
2638
+ }
2639
+ }
2640
+ if (drifted.length > 0) {
2641
+ ctx.logger?.warn?.("[external-validation] background drift detected", {
2642
+ datasource,
2643
+ objects: drifted.map((r) => r.object)
2644
+ });
2645
+ }
2646
+ return drifted.length;
2647
+ }
2648
+ };
2649
+ function createExternalValidationPlugin() {
2650
+ return new ExternalValidationPlugin();
2651
+ }
2652
+ async function resolveOnMismatch(metadata, datasource) {
2653
+ try {
2654
+ const ds = await metadata?.get?.("datasource", datasource);
2655
+ return ds?.external?.validation?.onMismatch ?? "fail";
2656
+ } catch {
2657
+ return "fail";
2658
+ }
2659
+ }
2660
+ function safeGet(ctx, name) {
2661
+ try {
2662
+ return ctx.getService(name);
2663
+ } catch {
2664
+ return void 0;
2665
+ }
2666
+ }
2667
+
2628
2668
  // src/http-dispatcher.ts
2629
2669
  var import_core2 = require("@objectstack/core");
2630
- var import_types3 = require("@objectstack/types");
2631
- var import_system = require("@objectstack/spec/system");
2632
- var import_shared = require("@objectstack/spec/shared");
2670
+ var import_system2 = require("@objectstack/spec/system");
2671
+ var import_shared2 = require("@objectstack/spec/shared");
2633
2672
  init_package_state_store();
2634
2673
 
2635
2674
  // src/security/resolve-execution-context.ts
@@ -3092,7 +3131,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3092
3131
  }
3093
3132
  }
3094
3133
  try {
3095
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
3134
+ const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
3096
3135
  const sessionData = await authService?.api?.getSession?.({
3097
3136
  headers: context.request?.headers
3098
3137
  });
@@ -3173,7 +3212,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3173
3212
  let userId;
3174
3213
  let activeOrganizationId;
3175
3214
  try {
3176
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
3215
+ const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
3177
3216
  const sessionData = await authService?.api?.getSession?.({
3178
3217
  headers: context.request?.headers
3179
3218
  });
@@ -3242,21 +3281,21 @@ var _HttpDispatcher = class _HttpDispatcher {
3242
3281
  queueSvc,
3243
3282
  jobSvc
3244
3283
  ] = await Promise.all([
3245
- this.resolveService(import_system.CoreServiceName.enum.auth),
3246
- this.resolveService(import_system.CoreServiceName.enum.graphql),
3247
- this.resolveService(import_system.CoreServiceName.enum.search),
3248
- this.resolveService(import_system.CoreServiceName.enum.realtime),
3249
- this.resolveService(import_system.CoreServiceName.enum["file-storage"]),
3250
- this.resolveService(import_system.CoreServiceName.enum.analytics),
3251
- this.resolveService(import_system.CoreServiceName.enum.workflow),
3252
- this.resolveService(import_system.CoreServiceName.enum.ai),
3253
- this.resolveService(import_system.CoreServiceName.enum.notification),
3254
- this.resolveService(import_system.CoreServiceName.enum.i18n),
3255
- this.resolveService(import_system.CoreServiceName.enum.ui),
3256
- this.resolveService(import_system.CoreServiceName.enum.automation),
3257
- this.resolveService(import_system.CoreServiceName.enum.cache),
3258
- this.resolveService(import_system.CoreServiceName.enum.queue),
3259
- this.resolveService(import_system.CoreServiceName.enum.job)
3284
+ this.resolveService(import_system2.CoreServiceName.enum.auth),
3285
+ this.resolveService(import_system2.CoreServiceName.enum.graphql),
3286
+ this.resolveService(import_system2.CoreServiceName.enum.search),
3287
+ this.resolveService(import_system2.CoreServiceName.enum.realtime),
3288
+ this.resolveService(import_system2.CoreServiceName.enum["file-storage"]),
3289
+ this.resolveService(import_system2.CoreServiceName.enum.analytics),
3290
+ this.resolveService(import_system2.CoreServiceName.enum.workflow),
3291
+ this.resolveService(import_system2.CoreServiceName.enum.ai),
3292
+ this.resolveService(import_system2.CoreServiceName.enum.notification),
3293
+ this.resolveService(import_system2.CoreServiceName.enum.i18n),
3294
+ this.resolveService(import_system2.CoreServiceName.enum.ui),
3295
+ this.resolveService(import_system2.CoreServiceName.enum.automation),
3296
+ this.resolveService(import_system2.CoreServiceName.enum.cache),
3297
+ this.resolveService(import_system2.CoreServiceName.enum.queue),
3298
+ this.resolveService(import_system2.CoreServiceName.enum.job)
3260
3299
  ]);
3261
3300
  const hasAuth = !!authSvc;
3262
3301
  const hasGraphQL = !!(graphqlSvc || this.kernel.graphql);
@@ -3373,7 +3412,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3373
3412
  * path: sub-path after /auth/
3374
3413
  */
3375
3414
  async handleAuth(path, method, body, context) {
3376
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
3415
+ const authService = await this.getService(import_system2.CoreServiceName.enum.auth);
3377
3416
  if (authService && typeof authService.handler === "function") {
3378
3417
  const response = await authService.handler(context.request, context.response);
3379
3418
  return { handled: true, result: response };
@@ -3457,10 +3496,21 @@ var _HttpDispatcher = class _HttpDispatcher {
3457
3496
  }
3458
3497
  return { handled: true, response: this.success({ types: ["object", "app", "plugin"] }) };
3459
3498
  }
3499
+ if (parts.length === 4 && (parts[0] === "objects" || parts[0] === "object") && parts[2] === "state" && (!method || method === "GET")) {
3500
+ const name = parts[1];
3501
+ const field = parts[3];
3502
+ const from = query?.from !== void 0 ? String(query.from) : void 0;
3503
+ const qlService = await this.getObjectQLService();
3504
+ const schema = qlService?.registry?.getObject(name);
3505
+ if (!schema) return { handled: true, response: this.error("Object not found", 404) };
3506
+ const { legalNextStates } = await import("@objectstack/objectql");
3507
+ const next = from === void 0 ? null : legalNextStates(schema, field, from);
3508
+ return { handled: true, response: this.success({ object: name, field, from: from ?? null, next }) };
3509
+ }
3460
3510
  if (parts.length >= 3 && parts[parts.length - 1] === "published" && (!method || method === "GET")) {
3461
3511
  const type = parts[0];
3462
3512
  const name = parts.slice(1, -1).join("/");
3463
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3513
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3464
3514
  if (metadataService && typeof metadataService.getPublished === "function") {
3465
3515
  const data = await metadataService.getPublished(type, name);
3466
3516
  if (data === void 0) return { handled: true, response: this.error("Not found", 404) };
@@ -3534,7 +3584,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3534
3584
  }
3535
3585
  return { handled: true, response: this.error("Not found", 404) };
3536
3586
  }
3537
- const singularType = (0, import_shared.pluralToSingular)(type);
3587
+ const singularType = (0, import_shared2.pluralToSingular)(type);
3538
3588
  const protocol = await this.resolveService("protocol");
3539
3589
  if (protocol && typeof protocol.getMetaItem === "function") {
3540
3590
  try {
@@ -3571,7 +3621,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3571
3621
  } catch {
3572
3622
  }
3573
3623
  }
3574
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3624
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3575
3625
  if (metadataService && typeof metadataService.list === "function") {
3576
3626
  try {
3577
3627
  let items = await metadataService.list(typeOrName);
@@ -3705,7 +3755,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3705
3755
  * path: sub-path after /analytics/
3706
3756
  */
3707
3757
  async handleAnalytics(path, method, body, _context) {
3708
- const analyticsService = await this.getService(import_system.CoreServiceName.enum.analytics);
3758
+ const analyticsService = await this.getService(import_system2.CoreServiceName.enum.analytics);
3709
3759
  if (!analyticsService) return { handled: false };
3710
3760
  const m = method.toUpperCase();
3711
3761
  const subPath = path.replace(/^\/+/, "");
@@ -3735,7 +3785,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3735
3785
  * GET /labels/:object?locale=xx → getFieldLabels (locale from query)
3736
3786
  */
3737
3787
  async handleI18n(path, method, query, _context) {
3738
- const i18nService = await this.getService(import_system.CoreServiceName.enum.i18n);
3788
+ const i18nService = await this.getService(import_system2.CoreServiceName.enum.i18n);
3739
3789
  if (!i18nService) return { handled: true, response: this.error("i18n service not available", 501) };
3740
3790
  const m = method.toUpperCase();
3741
3791
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -3845,7 +3895,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3845
3895
  }
3846
3896
  if (parts.length === 2 && parts[1] === "publish" && m === "POST") {
3847
3897
  const id = decodeURIComponent(parts[0]);
3848
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3898
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3849
3899
  if (metadataService && typeof metadataService.publishPackage === "function") {
3850
3900
  const result = await metadataService.publishPackage(id, body || {});
3851
3901
  return { handled: true, response: this.success(result) };
@@ -3854,7 +3904,7 @@ var _HttpDispatcher = class _HttpDispatcher {
3854
3904
  }
3855
3905
  if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
3856
3906
  const id = decodeURIComponent(parts[0]);
3857
- const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
3907
+ const metadataService = await this.getService(import_system2.CoreServiceName.enum.metadata);
3858
3908
  if (metadataService && typeof metadataService.revertPackage === "function") {
3859
3909
  await metadataService.revertPackage(id);
3860
3910
  return { handled: true, response: this.success({ success: true }) };
@@ -3929,13 +3979,13 @@ var _HttpDispatcher = class _HttpDispatcher {
3929
3979
  }
3930
3980
  return out;
3931
3981
  };
3932
- const exportPluralKeys = Object.keys(import_shared.PLURAL_TO_SINGULAR).filter(
3982
+ const exportPluralKeys = Object.keys(import_shared2.PLURAL_TO_SINGULAR).filter(
3933
3983
  (k) => k !== "datasources" && k !== "emailTemplates"
3934
3984
  );
3935
3985
  const manifest = {};
3936
3986
  let total = 0;
3937
3987
  for (const plural of exportPluralKeys) {
3938
- const singular = import_shared.PLURAL_TO_SINGULAR[plural];
3988
+ const singular = import_shared2.PLURAL_TO_SINGULAR[plural];
3939
3989
  let items = [];
3940
3990
  try {
3941
3991
  const res = await protocol.getMetaItems({ type: singular, packageId, organizationId });
@@ -4007,7 +4057,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4007
4057
  */
4008
4058
  async resolveActiveOrganizationId(context) {
4009
4059
  try {
4010
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
4060
+ const authService = await this.resolveService(import_system2.CoreServiceName.enum.auth);
4011
4061
  const rawHeaders = context.request?.headers;
4012
4062
  let headers = rawHeaders;
4013
4063
  if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
@@ -4030,1305 +4080,14 @@ var _HttpDispatcher = class _HttpDispatcher {
4030
4080
  return void 0;
4031
4081
  }
4032
4082
  }
4033
- async resolveCallerUserId(context) {
4034
- try {
4035
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
4036
- const rawHeaders = context.request?.headers;
4037
- let headers = rawHeaders;
4038
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4039
- try {
4040
- const h = new Headers();
4041
- for (const [k, v] of Object.entries(rawHeaders)) {
4042
- if (v == null) continue;
4043
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
4044
- }
4045
- headers = h;
4046
- } catch {
4047
- headers = rawHeaders;
4048
- }
4049
- }
4050
- const sessionData = await (authService?.auth?.api?.getSession ?? authService?.api?.getSession)?.call(
4051
- authService?.auth?.api ?? authService?.api,
4052
- { headers }
4053
- );
4054
- return sessionData?.user?.id ?? sessionData?.session?.userId;
4055
- } catch (e) {
4056
- return void 0;
4057
- }
4058
- }
4059
- async handleCloud(path, method, body, query, _context) {
4060
- const m = method.toUpperCase();
4061
- const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
4062
- const qlService = await this.getObjectQLService();
4063
- const ql = qlService ?? await this.resolveService("objectql");
4064
- if (!ql) {
4065
- return { handled: true, response: this.error("Project service not available (ObjectQL missing)", 503) };
4066
- }
4067
- const ENV = "sys_environment";
4068
- const CRED = "sys_environment_credential";
4069
- const MEM = "sys_environment_member";
4070
- const PKG_INSTALL = "sys_package_installation";
4071
- const PKG = "sys_package";
4072
- const PKG_VERSION = "sys_package_version";
4073
- const ensureSysPackage = async (manifestId, ownerOrgId, createdBy, manifest) => {
4074
- const existing = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
4075
- if (existing?.id) return existing.id;
4076
- const id = randomUUID();
4077
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4078
- await ql.insert(PKG, {
4079
- id,
4080
- manifest_id: manifestId,
4081
- owner_org_id: ownerOrgId,
4082
- display_name: manifest?.name ?? manifestId,
4083
- description: manifest?.description ?? null,
4084
- visibility: "private",
4085
- created_by: createdBy,
4086
- created_at: nowIso,
4087
- updated_at: nowIso
4088
- });
4089
- return id;
4090
- };
4091
- const ensureSysPackageVersion = async (packageId, version, createdBy, manifest) => {
4092
- const existing = await ql.findOne(PKG_VERSION, {
4093
- where: { package_id: packageId, version }
4094
- });
4095
- if (existing?.id) return existing.id;
4096
- const id = randomUUID();
4097
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4098
- await ql.insert(PKG_VERSION, {
4099
- id,
4100
- package_id: packageId,
4101
- version,
4102
- status: "published",
4103
- manifest_json: manifest ? JSON.stringify(manifest) : null,
4104
- is_pre_release: false,
4105
- published_at: nowIso,
4106
- published_by: createdBy,
4107
- created_by: createdBy,
4108
- created_at: nowIso,
4109
- updated_at: nowIso
4110
- });
4111
- return id;
4112
- };
4113
- const findInstallByManifestId = async (envId, manifestId) => {
4114
- const pkgRow = await ql.findOne(PKG, { where: { manifest_id: manifestId } });
4115
- if (!pkgRow?.id) return null;
4116
- return await ql.findOne(PKG_INSTALL, {
4117
- where: { environment_id: envId, package_id: pkgRow.id }
4118
- });
4119
- };
4120
- const toShortName = (driverId) => {
4121
- const prefix = "com.objectstack.driver.";
4122
- return driverId.startsWith(prefix) ? driverId.slice(prefix.length) : driverId;
4123
- };
4124
- const listRegisteredDrivers = () => {
4125
- const services = this.getServicesMap();
4126
- const registry = services["project-provisioning-adapters"];
4127
- if (registry && typeof registry.list === "function") {
4128
- try {
4129
- const adapters = registry.list();
4130
- const seen = /* @__PURE__ */ new Set();
4131
- const drivers2 = [];
4132
- for (const adapter of adapters ?? []) {
4133
- const name = adapter?.driver;
4134
- if (!name || seen.has(name)) continue;
4135
- seen.add(name);
4136
- drivers2.push({ name, driverId: `com.objectstack.driver.${name}` });
4137
- }
4138
- if (drivers2.length > 0) return drivers2;
4139
- } catch {
4140
- }
4141
- }
4142
- const drivers = [];
4143
- for (const [serviceKey, svc] of Object.entries(services)) {
4144
- if (!serviceKey.startsWith("driver.")) continue;
4145
- const raw = serviceKey.slice("driver.".length);
4146
- if (!raw || raw === "unknown") continue;
4147
- const driverId = svc?.name ?? raw;
4148
- drivers.push({ name: toShortName(driverId), driverId });
4149
- }
4150
- return drivers;
4151
- };
4152
- const resolveDriver = (requested) => {
4153
- const registered = listRegisteredDrivers();
4154
- if (requested) {
4155
- const wanted = String(requested).toLowerCase();
4156
- return registered.find((d) => d.name === wanted || d.driverId === wanted);
4157
- }
4158
- return registered.find((d) => d.name === "turso") ?? registered.find((d) => d.name === "memory") ?? registered[0];
4159
- };
4160
- const buildDatabaseUrl = (driverName, environmentId) => {
4161
- const dbName = `env-${environmentId}`;
4162
- switch (driverName) {
4163
- case "memory":
4164
- return `memory://${dbName}`;
4165
- case "turso":
4166
- return `libsql://${dbName}.mock-turso.local`;
4167
- default:
4168
- return `${driverName}://${dbName}`;
4169
- }
4170
- };
4171
- const getRealAdapter = async (driverName) => {
4172
- try {
4173
- const registry = await this.resolveService("project-provisioning-adapters");
4174
- const aliases = { sql: "sqlite" };
4175
- const effective = aliases[driverName] ?? driverName;
4176
- return registry?.get?.(effective) ?? registry?.get?.(driverName);
4177
- } catch {
4178
- return void 0;
4179
- }
4180
- };
4181
- const findOne = async (obj, where) => {
4182
- let rows = await ql.find(obj, { where });
4183
- if (rows && rows.value) rows = rows.value;
4184
- if (!Array.isArray(rows)) return void 0;
4185
- return rows[0];
4186
- };
4187
- const cleanProjectRow = (row) => {
4188
- if (!row) return row;
4189
- let metadata = row.metadata;
4190
- if (typeof metadata === "string") {
4191
- try {
4192
- metadata = JSON.parse(metadata);
4193
- } catch {
4194
- }
4195
- }
4196
- return { ...row, metadata };
4197
- };
4198
- try {
4199
- if (parts.length === 1 && parts[0] === "drivers" && m === "GET") {
4200
- const drivers = listRegisteredDrivers();
4201
- return { handled: true, response: this.success({ drivers, total: drivers.length }) };
4202
- }
4203
- if (parts.length === 1 && parts[0] === "templates" && m === "GET") {
4204
- try {
4205
- const seeder = await this.resolveService("template-seeder");
4206
- const templates = seeder?.listTemplates?.() ?? [];
4207
- return { handled: true, response: this.success({ templates, total: templates.length }) };
4208
- } catch (err) {
4209
- try {
4210
- console.error("[HttpDispatcher] /cloud/templates: failed to resolve template-seeder:", err?.message ?? err);
4211
- } catch {
4212
- }
4213
- return { handled: true, response: this.success({ templates: [], total: 0 }) };
4214
- }
4215
- }
4216
- if (parts.length === 3 && parts[0] === "admin" && parts[1] === "platform-sso" && parts[2] === "backfill" && m === "POST") {
4217
- const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4218
- if (!baseSecret) {
4219
- return { handled: true, response: this.error("OS_AUTH_SECRET not configured on this worker", 503) };
4220
- }
4221
- const rawHeaders = _context?.request?.headers;
4222
- let authHeader;
4223
- if (rawHeaders && typeof rawHeaders.get === "function") {
4224
- authHeader = rawHeaders.get("authorization") ?? void 0;
4225
- } else if (rawHeaders && typeof rawHeaders === "object") {
4226
- authHeader = rawHeaders["authorization"] ?? rawHeaders["Authorization"];
4227
- }
4228
- const presented = typeof authHeader === "string" && authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : "";
4229
- if (!presented || presented !== baseSecret) {
4230
- return { handled: true, response: this.error("forbidden: Bearer token must match OS_AUTH_SECRET", 403) };
4231
- }
4232
- try {
4233
- const { backfillPlatformSsoClients: backfillPlatformSsoClients2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4234
- const result = await backfillPlatformSsoClients2({
4235
- ql,
4236
- baseSecret,
4237
- logger: console
4238
- });
4239
- let sample = [];
4240
- let total = 0;
4241
- try {
4242
- const rows = await ql.find("sys_oauth_application", { limit: 5 }, { context: { isSystem: true } });
4243
- const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
4244
- sample = list;
4245
- total = typeof rows?.total === "number" ? rows.total : list.length;
4246
- } catch (e) {
4247
- sample = [{ _readErr: e?.message ?? String(e) }];
4248
- }
4249
- return { handled: true, response: this.success({ ...result, total, sample }) };
4250
- } catch (err) {
4251
- return { handled: true, response: this.error(`backfill failed: ${err?.message ?? String(err)}`, 500) };
4252
- }
4253
- }
4254
- if (parts.length === 1 && parts[0] === "projects" && m === "GET") {
4255
- const where = {};
4256
- if (query?.organizationId) where.organization_id = query.organizationId;
4257
- if (query?.status) where.status = query.status;
4258
- let rows = await ql.find(ENV, Object.keys(where).length ? { where } : void 0);
4259
- if (rows && rows.value) rows = rows.value;
4260
- const projects = (Array.isArray(rows) ? rows : []).map(cleanProjectRow);
4261
- return { handled: true, response: this.success({ projects, total: projects.length }) };
4262
- }
4263
- if (parts.length === 1 && parts[0] === "projects" && m === "POST") {
4264
- const req = body || {};
4265
- if (req.organization_id === "__session__" || req.created_by === "__session__") {
4266
- try {
4267
- const userId = await this.resolveCallerUserId(_context);
4268
- if (req.created_by === "__session__") {
4269
- req.created_by = userId ?? "system";
4270
- }
4271
- if (req.organization_id === "__session__") {
4272
- const authService = await this.resolveService(import_system.CoreServiceName.enum.auth);
4273
- const rawHeaders = _context?.request?.headers;
4274
- let headers = rawHeaders;
4275
- if (rawHeaders && typeof rawHeaders === "object" && typeof rawHeaders.get !== "function") {
4276
- const h = new Headers();
4277
- for (const [k, v] of Object.entries(rawHeaders)) {
4278
- if (v == null) continue;
4279
- h.set(k, Array.isArray(v) ? v.join(", ") : String(v));
4280
- }
4281
- headers = h;
4282
- }
4283
- const apiObj = authService?.auth?.api ?? authService?.api;
4284
- const sessionData = await apiObj?.getSession?.call(apiObj, { headers });
4285
- req.organization_id = sessionData?.session?.activeOrganizationId ?? void 0;
4286
- }
4287
- } catch {
4288
- }
4289
- }
4290
- if (!req.organization_id || !req.display_name) {
4291
- return { handled: true, response: this.error("organization_id and display_name are required", 400) };
4292
- }
4293
- const environmentId = randomUUID();
4294
- const credentialId = randomUUID();
4295
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4296
- const resolved = resolveDriver(req.driver);
4297
- if (!resolved) {
4298
- const available = listRegisteredDrivers().map((d) => d.name);
4299
- if (req.driver) {
4300
- return {
4301
- handled: true,
4302
- response: this.error(
4303
- `Unknown driver '${req.driver}'. Available drivers: [${available.join(", ") || "none"}]`,
4304
- 400
4305
- )
4306
- };
4307
- }
4308
- return {
4309
- handled: true,
4310
- response: this.error(
4311
- "No ObjectQL driver is registered. Register at least one DriverPlugin (e.g. InMemoryDriver or SqlDriver).",
4312
- 503
4313
- )
4314
- };
4315
- }
4316
- const driver = resolved.name;
4317
- let plaintextSecret = `mock-token-${environmentId}`;
4318
- let computedHostname = req.hostname;
4319
- if (!computedHostname) {
4320
- const shortId = environmentId.slice(0, 8);
4321
- try {
4322
- const orgRow = await findOne("sys_organization", { id: req.organization_id });
4323
- const orgSlug = orgRow?.slug || req.organization_id;
4324
- const rootDomain = (0, import_core2.getEnv)("OS_ROOT_DOMAIN") ?? (0, import_core2.getEnv)("ROOT_DOMAIN", "objectstack.app");
4325
- computedHostname = `${orgSlug}-${shortId}.${rootDomain}`;
4326
- } catch {
4327
- computedHostname = `${req.organization_id}-${shortId}.objectstack.app`;
4328
- }
4329
- }
4330
- try {
4331
- const existing = await findOne("sys_environment", {
4332
- hostname: computedHostname
4333
- });
4334
- if (existing && existing.id !== environmentId) {
4335
- return {
4336
- handled: true,
4337
- response: this.error(
4338
- `Hostname '${computedHostname}' is already in use by another project.`,
4339
- 409,
4340
- { code: "HOSTNAME_TAKEN", hostname: computedHostname }
4341
- )
4342
- };
4343
- }
4344
- } catch {
4345
- }
4346
- const baseMetadata = { ...req.metadata ?? {} };
4347
- const simulateFailure = Boolean(baseMetadata.__simulateFailure);
4348
- const simulateDelayMs = Number(baseMetadata.__simulateDelayMs ?? 1500);
4349
- try {
4350
- let ownerUserId = req.created_by && req.created_by !== "system" ? String(req.created_by) : void 0;
4351
- if (!ownerUserId) {
4352
- ownerUserId = await this.resolveCallerUserId(_context);
4353
- }
4354
- if (ownerUserId) {
4355
- const userRow = await ql.find("sys_user", { where: { id: ownerUserId } });
4356
- const userRows = Array.isArray(userRow) ? userRow : userRow?.value ?? [];
4357
- const u = Array.isArray(userRows) && userRows.length > 0 ? userRows[0] : null;
4358
- if (u?.email) {
4359
- baseMetadata.ownerSeed = {
4360
- userId: String(ownerUserId),
4361
- email: String(u.email),
4362
- name: u.name ? String(u.name) : null,
4363
- image: u.image ? String(u.image) : null
4364
- };
4365
- }
4366
- }
4367
- } catch {
4368
- }
4369
- try {
4370
- const orgRow = await ql.find("sys_organization", { where: { id: req.organization_id } });
4371
- const orgRows = Array.isArray(orgRow) ? orgRow : orgRow?.value ?? [];
4372
- const org = Array.isArray(orgRows) && orgRows.length > 0 ? orgRows[0] : null;
4373
- if (org?.id && org?.name) {
4374
- baseMetadata.orgSeed = {
4375
- id: String(org.id),
4376
- name: String(org.name),
4377
- slug: org.slug ? String(org.slug) : null,
4378
- logo: org.logo ? String(org.logo) : null
4379
- };
4380
- }
4381
- } catch {
4382
- }
4383
- await ql.insert(ENV, {
4384
- id: environmentId,
4385
- organization_id: req.organization_id,
4386
- display_name: req.display_name,
4387
- is_default: req.is_default ?? false,
4388
- is_system: req.is_system ?? false,
4389
- plan: req.plan ?? "free",
4390
- status: "provisioning",
4391
- created_by: req.created_by ?? "system",
4392
- metadata: JSON.stringify(baseMetadata),
4393
- created_at: nowIso,
4394
- updated_at: nowIso,
4395
- database_url: null,
4396
- database_driver: driver,
4397
- storage_limit_mb: req.storage_limit_mb ?? 1024,
4398
- provisioned_at: null,
4399
- hostname: computedHostname,
4400
- visibility: (() => {
4401
- const raw = String(req.visibility ?? "private");
4402
- return raw === "unlisted" ? "private" : raw;
4403
- })()
4404
- });
4405
- try {
4406
- const { seedPlatformSsoClient: seedPlatformSsoClient2 } = await Promise.resolve().then(() => (init_platform_sso(), platform_sso_exports));
4407
- const baseSecret = ((0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
4408
- if (baseSecret) {
4409
- await seedPlatformSsoClient2({
4410
- ql,
4411
- environmentId,
4412
- hostname: computedHostname,
4413
- baseSecret,
4414
- logger: console
4415
- });
4416
- }
4417
- } catch (ssoErr) {
4418
- console.warn?.("[http-dispatcher] platform SSO seed failed (non-fatal)", {
4419
- environmentId,
4420
- error: ssoErr?.message
4421
- });
4422
- }
4423
- const runProvisioning = async () => {
4424
- try {
4425
- if (simulateDelayMs > 0) {
4426
- await new Promise((r) => setTimeout(r, simulateDelayMs));
4427
- }
4428
- if (simulateFailure) {
4429
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4430
- }
4431
- let databaseUrl;
4432
- try {
4433
- const adapter = await getRealAdapter(driver);
4434
- if (adapter) {
4435
- const result = await adapter.createDatabase({
4436
- environmentId,
4437
- databaseName: `p-${environmentId.replace(/-/g, "").slice(0, 24)}`,
4438
- region: "us-east-1",
4439
- storageLimitMb: req.storage_limit_mb ?? 1024
4440
- });
4441
- databaseUrl = result.databaseUrl;
4442
- if (result.plaintextSecret) plaintextSecret = result.plaintextSecret;
4443
- } else {
4444
- databaseUrl = buildDatabaseUrl(driver, environmentId);
4445
- }
4446
- } catch (adapterErr) {
4447
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4448
- }
4449
- const seedStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4450
- await ql.update(
4451
- ENV,
4452
- {
4453
- database_url: databaseUrl,
4454
- updated_at: seedStartedAt
4455
- },
4456
- { where: { id: environmentId } }
4457
- );
4458
- await ql.insert(CRED, {
4459
- id: credentialId,
4460
- environment_id: environmentId,
4461
- secret_ciphertext: plaintextSecret,
4462
- encryption_key_id: "noop",
4463
- authorization: "full_access",
4464
- status: "active",
4465
- created_at: seedStartedAt,
4466
- updated_at: seedStartedAt
4467
- });
4468
- const templateId = req.template_id ?? "blank";
4469
- if (templateId !== "blank") {
4470
- try {
4471
- const seeder = await this.resolveService("template-seeder");
4472
- if (seeder) {
4473
- await seeder.seed({ environmentId, templateId });
4474
- }
4475
- } catch (seedErr) {
4476
- const seedMessage = seedErr instanceof Error ? seedErr.message : String(seedErr);
4477
- try {
4478
- const existing = await findOne(ENV, { id: environmentId });
4479
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4480
- await ql.update(
4481
- ENV,
4482
- {
4483
- metadata: JSON.stringify({
4484
- ...existingMeta,
4485
- templateSeedError: { message: seedMessage, templateId }
4486
- })
4487
- },
4488
- { where: { id: environmentId } }
4489
- );
4490
- } catch {
4491
- }
4492
- }
4493
- }
4494
- const artifactPathRaw = baseMetadata.artifact_path;
4495
- if (typeof artifactPathRaw === "string" && artifactPathRaw.length > 0) {
4496
- try {
4497
- const path2 = await import("path");
4498
- const { isHttpUrl: isHttpUrl2, loadArtifactBundle: loadArtifactBundle2 } = await Promise.resolve().then(() => (init_load_artifact_bundle(), load_artifact_bundle_exports));
4499
- const root = process.env.OS_PROJECT_ARTIFACT_ROOT ?? process.cwd();
4500
- const resolved2 = isHttpUrl2(artifactPathRaw) ? artifactPathRaw : path2.isAbsolute(artifactPathRaw) ? artifactPathRaw : path2.resolve(root, artifactPathRaw);
4501
- const bundle = await loadArtifactBundle2(resolved2, { tag: "[bind-artifact]" });
4502
- if (!bundle) {
4503
- throw new Error(`failed to load artifact bundle at '${resolved2}'`);
4504
- }
4505
- const seeder = await this.resolveService("template-seeder");
4506
- if (seeder?.seedBundle) {
4507
- await seeder.seedBundle({ environmentId, bundle });
4508
- } else {
4509
- throw new Error("template-seeder.seedBundle is unavailable");
4510
- }
4511
- } catch (bindErr) {
4512
- const bindMessage = bindErr instanceof Error ? bindErr.message : String(bindErr);
4513
- try {
4514
- const existing = await findOne(ENV, { id: environmentId });
4515
- const existingMeta = typeof existing?.metadata === "string" ? JSON.parse(existing.metadata) : existing?.metadata ?? {};
4516
- await ql.update(
4517
- ENV,
4518
- {
4519
- metadata: JSON.stringify({
4520
- ...existingMeta,
4521
- artifactBindError: { message: bindMessage, artifactPath: artifactPathRaw }
4522
- })
4523
- },
4524
- { where: { id: environmentId } }
4525
- );
4526
- } catch {
4527
- }
4528
- }
4529
- }
4530
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
4531
- await ql.update(
4532
- ENV,
4533
- {
4534
- status: "active",
4535
- provisioned_at: finishedAt,
4536
- updated_at: finishedAt
4537
- },
4538
- { where: { id: environmentId } }
4539
- );
4540
- } catch (err) {
4541
- const message = err instanceof Error ? err.message : String(err);
4542
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4543
- await ql.update(
4544
- ENV,
4545
- {
4546
- status: "failed",
4547
- metadata: JSON.stringify({
4548
- ...baseMetadata,
4549
- provisioningError: { message, failedAt }
4550
- }),
4551
- updated_at: failedAt
4552
- },
4553
- { where: { id: environmentId } }
4554
- );
4555
- }
4556
- };
4557
- const provisionSyncEnv = process.env.OS_PROVISION_SYNC;
4558
- const onServerless = !!(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY || process.env.CF_PAGES);
4559
- const syncProvisioning = provisionSyncEnv === void 0 ? onServerless : provisionSyncEnv !== "0" && provisionSyncEnv !== "false";
4560
- if (syncProvisioning) {
4561
- await runProvisioning();
4562
- } else {
4563
- void runProvisioning();
4564
- }
4565
- const project = cleanProjectRow(await findOne(ENV, { id: environmentId }));
4566
- const res = this.success({ project });
4567
- res.status = syncProvisioning ? 201 : 202;
4568
- return { handled: true, response: res };
4569
- }
4570
- if (parts.length === 2 && parts[0] === "projects") {
4571
- const id = decodeURIComponent(parts[1]);
4572
- if (m === "GET") {
4573
- const envRow = await findOne(ENV, { id });
4574
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4575
- const credRow = await findOne(CRED, { environment_id: id, status: "active" });
4576
- const callerUserId = await this.resolveCallerUserId(_context);
4577
- const membership = callerUserId ? await findOne(MEM, { environment_id: id, user_id: callerUserId }) : await findOne(MEM, { environment_id: id });
4578
- const credMeta = credRow ? {
4579
- id: credRow.id,
4580
- status: credRow.status,
4581
- authorization: credRow.authorization,
4582
- activatedAt: credRow.created_at,
4583
- expiresAt: credRow.expires_at
4584
- } : void 0;
4585
- const project = cleanProjectRow(envRow);
4586
- const database = project.database_url ? {
4587
- driver: project.database_driver,
4588
- database_name: `env-${project.id}`,
4589
- database_url: project.database_url,
4590
- storage_limit_mb: project.storage_limit_mb,
4591
- provisioned_at: project.provisioned_at
4592
- } : void 0;
4593
- return {
4594
- handled: true,
4595
- response: this.success({ project, database, credential: credMeta, membership })
4596
- };
4597
- }
4598
- if (m === "PATCH") {
4599
- const patch = {};
4600
- if (body?.display_name !== void 0) patch.display_name = body.display_name;
4601
- if (body?.plan !== void 0) patch.plan = body.plan;
4602
- if (body?.status !== void 0) patch.status = body.status;
4603
- if (body?.is_default !== void 0) patch.is_default = body.is_default;
4604
- if (body?.visibility !== void 0) {
4605
- let v = String(body.visibility);
4606
- if (v === "unlisted") v = "private";
4607
- if (!["private", "public"].includes(v)) {
4608
- return { handled: true, response: this.error(`Invalid visibility '${v}' (expected private | public)`, 400) };
4609
- }
4610
- patch.visibility = v;
4611
- }
4612
- if (body?.metadata !== void 0) patch.metadata = JSON.stringify(body.metadata);
4613
- patch.updated_at = (/* @__PURE__ */ new Date()).toISOString();
4614
- await ql.update(ENV, patch, { where: { id } });
4615
- const envRow = await findOne(ENV, { id });
4616
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4617
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow) }) };
4618
- }
4619
- if (m === "DELETE") {
4620
- const force = query?.force === "1" || query?.force === "true" || body?.force === true;
4621
- const result = await this.deleteProjectCascade(id, { ql, findOne, getRealAdapter, force });
4622
- if (!result.ok) {
4623
- return { handled: true, response: this.error(result.error ?? "Delete failed", result.status ?? 500) };
4624
- }
4625
- return { handled: true, response: this.success({ deleted: true, environmentId: id, warnings: result.warnings }) };
4626
- }
4627
- }
4628
- if (parts.length === 2 && parts[0] === "organizations" && m === "DELETE") {
4629
- const orgId = decodeURIComponent(parts[1]);
4630
- let projectRows = [];
4631
- try {
4632
- let rows = await ql.find(ENV, { where: { organization_id: orgId } });
4633
- if (rows && rows.value) rows = rows.value;
4634
- projectRows = Array.isArray(rows) ? rows : [];
4635
- } catch {
4636
- projectRows = [];
4637
- }
4638
- const warnings = [];
4639
- let deletedProjects = 0;
4640
- for (const row of projectRows) {
4641
- const pid = row?.id;
4642
- if (!pid) continue;
4643
- try {
4644
- const r = await this.deleteProjectCascade(pid, { ql, findOne, getRealAdapter, force: true });
4645
- if (r.ok) deletedProjects++;
4646
- if (r.warnings?.length) warnings.push(...r.warnings);
4647
- if (!r.ok && r.error) warnings.push(`Project ${pid}: ${r.error}`);
4648
- } catch (err) {
4649
- warnings.push(
4650
- `Failed to delete project ${pid}: ${err instanceof Error ? err.message : String(err)}`
4651
- );
4652
- }
4653
- }
4654
- let orgDeleted = false;
4655
- try {
4656
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
4657
- const fn = authService?.api?.deleteOrganization;
4658
- if (typeof fn === "function") {
4659
- await fn.call(authService.api, {
4660
- body: { organizationId: orgId },
4661
- headers: _context?.request?.headers
4662
- });
4663
- orgDeleted = true;
4664
- }
4665
- } catch (err) {
4666
- warnings.push(
4667
- `auth.deleteOrganization failed: ${err instanceof Error ? err.message : String(err)}`
4668
- );
4669
- }
4670
- if (!orgDeleted) {
4671
- try {
4672
- await ql.delete("sys_organization", { where: { id: orgId } });
4673
- orgDeleted = true;
4674
- } catch (err) {
4675
- warnings.push(
4676
- `Failed to delete sys_organization row: ${err instanceof Error ? err.message : String(err)}`
4677
- );
4678
- }
4679
- }
4680
- return {
4681
- handled: true,
4682
- response: this.success({
4683
- deleted: orgDeleted,
4684
- organizationId: orgId,
4685
- deletedProjects,
4686
- warnings
4687
- })
4688
- };
4689
- }
4690
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "hostname" && (m === "POST" || m === "PUT")) {
4691
- const id = decodeURIComponent(parts[1]);
4692
- const hostname = body?.hostname;
4693
- if (!hostname || typeof hostname !== "string") {
4694
- return { handled: true, response: this.error("hostname is required", 400) };
4695
- }
4696
- const normalized = hostname.trim().toLowerCase();
4697
- if (!/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/.test(normalized)) {
4698
- return { handled: true, response: this.error("Invalid hostname format", 400) };
4699
- }
4700
- const envRow = await findOne(ENV, { id });
4701
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4702
- let existing;
4703
- try {
4704
- const rows = await ql.find(ENV, { where: { hostname: normalized } });
4705
- const arr = Array.isArray(rows) ? rows : rows?.value ?? [];
4706
- existing = arr.find((r) => r.id !== id);
4707
- } catch {
4708
- }
4709
- if (existing) {
4710
- return {
4711
- handled: true,
4712
- response: this.error(
4713
- `Hostname '${normalized}' is already in use by another project.`,
4714
- 409,
4715
- { code: "HOSTNAME_TAKEN", hostname: normalized }
4716
- )
4717
- };
4718
- }
4719
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
4720
- await ql.update(ENV, { hostname: normalized, updated_at: updatedAt }, { where: { id } });
4721
- if (this.envRegistry?.invalidate) {
4722
- try {
4723
- await this.envRegistry.invalidate(id);
4724
- } catch {
4725
- }
4726
- }
4727
- const updated = cleanProjectRow(await findOne(ENV, { id }));
4728
- return { handled: true, response: this.success({ project: updated }) };
4729
- }
4730
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "retry" && m === "POST") {
4731
- const id = decodeURIComponent(parts[1]);
4732
- const envRow = await findOne(ENV, { id });
4733
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4734
- if (envRow.status !== "failed" && envRow.status !== "provisioning") {
4735
- return {
4736
- handled: true,
4737
- response: this.error(
4738
- `Project '${id}' is '${envRow.status}'; only failed or provisioning projects can be retried.`,
4739
- 409
4740
- )
4741
- };
4742
- }
4743
- const driverName = envRow.database_driver;
4744
- const resolved = resolveDriver(driverName);
4745
- if (!resolved) {
4746
- return {
4747
- handled: true,
4748
- response: this.error(
4749
- `Driver '${driverName}' is no longer registered; retry aborted.`,
4750
- 503
4751
- )
4752
- };
4753
- }
4754
- let metadata = {};
4755
- if (envRow.metadata) {
4756
- if (typeof envRow.metadata === "string") {
4757
- try {
4758
- metadata = JSON.parse(envRow.metadata);
4759
- } catch {
4760
- metadata = {};
4761
- }
4762
- } else if (typeof envRow.metadata === "object") {
4763
- metadata = { ...envRow.metadata };
4764
- }
4765
- }
4766
- delete metadata.provisioningError;
4767
- const retryStartedAt = (/* @__PURE__ */ new Date()).toISOString();
4768
- await ql.update(
4769
- ENV,
4770
- {
4771
- status: "provisioning",
4772
- metadata: JSON.stringify(metadata),
4773
- updated_at: retryStartedAt
4774
- },
4775
- { where: { id } }
4776
- );
4777
- const simulateRetryFailure = Boolean(metadata.__simulateFailure);
4778
- const simulateRetryDelay = Number(metadata.__simulateDelayMs ?? 1500);
4779
- const runRetry = async () => {
4780
- try {
4781
- if (simulateRetryDelay > 0) {
4782
- await new Promise((r) => setTimeout(r, simulateRetryDelay));
4783
- }
4784
- if (simulateRetryFailure) {
4785
- throw new Error("Simulated provisioning failure (metadata.__simulateFailure=true)");
4786
- }
4787
- let databaseUrl;
4788
- let retrySecret = `mock-token-${id}`;
4789
- try {
4790
- const adapter = await getRealAdapter(resolved.name);
4791
- if (adapter) {
4792
- const result = await adapter.createDatabase({
4793
- environmentId: id,
4794
- databaseName: `p-${id.replace(/-/g, "").slice(0, 24)}`,
4795
- region: "us-east-1",
4796
- storageLimitMb: envRow.storage_limit_mb ?? 1024
4797
- });
4798
- databaseUrl = result.databaseUrl;
4799
- if (result.plaintextSecret) retrySecret = result.plaintextSecret;
4800
- } else {
4801
- databaseUrl = buildDatabaseUrl(resolved.name, id);
4802
- }
4803
- } catch (adapterErr) {
4804
- throw adapterErr instanceof Error ? adapterErr : new Error(String(adapterErr));
4805
- }
4806
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4807
- await ql.update(
4808
- ENV,
4809
- {
4810
- status: "active",
4811
- database_url: databaseUrl,
4812
- database_driver: resolved.name,
4813
- provisioned_at: nowIso,
4814
- updated_at: nowIso
4815
- },
4816
- { where: { id } }
4817
- );
4818
- const existingCred = await findOne(CRED, { environment_id: id, status: "active" });
4819
- if (!existingCred) {
4820
- await ql.insert(CRED, {
4821
- id: randomUUID(),
4822
- environment_id: id,
4823
- secret_ciphertext: retrySecret,
4824
- encryption_key_id: "noop",
4825
- authorization: "full_access",
4826
- status: "active",
4827
- created_at: nowIso,
4828
- updated_at: nowIso
4829
- });
4830
- }
4831
- } catch (err) {
4832
- const message = err instanceof Error ? err.message : String(err);
4833
- const failedAt = (/* @__PURE__ */ new Date()).toISOString();
4834
- await ql.update(
4835
- ENV,
4836
- {
4837
- status: "failed",
4838
- metadata: JSON.stringify({
4839
- ...metadata,
4840
- provisioningError: { message, failedAt }
4841
- }),
4842
- updated_at: failedAt
4843
- },
4844
- { where: { id } }
4845
- );
4846
- }
4847
- };
4848
- void runRetry();
4849
- const envAfter = cleanProjectRow(await findOne(ENV, { id }));
4850
- const retryRes = this.success({ project: envAfter });
4851
- retryRes.status = 202;
4852
- return { handled: true, response: retryRes };
4853
- }
4854
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "activate" && m === "POST") {
4855
- const id = decodeURIComponent(parts[1]);
4856
- const envRow = await findOne(ENV, { id });
4857
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4858
- return { handled: true, response: this.success({ project: cleanProjectRow(envRow), sessionUpdated: false }) };
4859
- }
4860
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "credentials" && parts[3] === "rotate" && m === "POST") {
4861
- const id = decodeURIComponent(parts[1]);
4862
- const plaintext = body?.plaintext;
4863
- if (!plaintext || typeof plaintext !== "string") {
4864
- return { handled: true, response: this.error("plaintext is required", 400) };
4865
- }
4866
- const envRow = await findOne(ENV, { id });
4867
- if (!envRow) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4868
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
4869
- let existing = await ql.find(CRED, { where: { environment_id: id, status: "active" } });
4870
- if (existing && existing.value) existing = existing.value;
4871
- for (const row of Array.isArray(existing) ? existing : []) {
4872
- await ql.update(CRED, {
4873
- status: "revoked",
4874
- revoked_at: nowIso,
4875
- updated_at: nowIso
4876
- }, { where: { id: row.id } });
4877
- }
4878
- const credentialId = randomUUID();
4879
- await ql.insert(CRED, {
4880
- id: credentialId,
4881
- environment_id: id,
4882
- secret_ciphertext: plaintext,
4883
- encryption_key_id: "noop",
4884
- authorization: "full_access",
4885
- status: "active",
4886
- created_at: nowIso,
4887
- updated_at: nowIso
4888
- });
4889
- const credential = await findOne(CRED, { id: credentialId });
4890
- const credMeta = credential ? {
4891
- id: credential.id,
4892
- status: credential.status,
4893
- authorization: credential.authorization,
4894
- activatedAt: credential.created_at
4895
- } : void 0;
4896
- return { handled: true, response: this.success({ credential: credMeta }) };
4897
- }
4898
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "GET") {
4899
- const id = decodeURIComponent(parts[1]);
4900
- let rows = await ql.find(MEM, { where: { environment_id: id } });
4901
- if (rows && rows.value) rows = rows.value;
4902
- const members = Array.isArray(rows) ? rows : [];
4903
- const userIds = Array.from(new Set(members.map((mem) => mem.user_id).filter(Boolean)));
4904
- const userMap = /* @__PURE__ */ new Map();
4905
- for (const uid of userIds) {
4906
- let row = null;
4907
- for (const tableName of ["sys_user", "user"]) {
4908
- try {
4909
- const u = await ql.findOne(tableName, { where: { id: uid } });
4910
- row = u?.value ?? u;
4911
- if (row) break;
4912
- } catch {
4913
- }
4914
- }
4915
- if (row) userMap.set(String(uid), {
4916
- id: row.id,
4917
- name: row.name ?? row.display_name,
4918
- email: row.email,
4919
- image: row.image ?? row.avatar_url
4920
- });
4921
- }
4922
- const enriched = members.map((mem) => ({
4923
- ...mem,
4924
- user: userMap.get(String(mem.user_id)) ?? void 0
4925
- }));
4926
- return { handled: true, response: this.success({ members: enriched }) };
4927
- }
4928
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "members" && m === "POST") {
4929
- const id = decodeURIComponent(parts[1]);
4930
- const project = await findOne(ENV, { id });
4931
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4932
- const callerId = await this.resolveCallerUserId(_context);
4933
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4934
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4935
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4936
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4937
- }
4938
- const email = typeof body?.email === "string" ? String(body.email).trim().toLowerCase() : null;
4939
- let inviteUserId = typeof body?.user_id === "string" ? String(body.user_id).trim() : null;
4940
- let role = String(body?.role ?? "member").trim().toLowerCase();
4941
- if (!["owner", "admin", "member", "viewer"].includes(role)) {
4942
- return { handled: true, response: this.error(`Invalid role '${role}' (expected owner | admin | member | viewer)`, 400) };
4943
- }
4944
- if (!email && !inviteUserId) {
4945
- return { handled: true, response: this.error("email or user_id is required", 400) };
4946
- }
4947
- if (!inviteUserId && email) {
4948
- let row = null;
4949
- for (const tableName of ["sys_user", "user"]) {
4950
- try {
4951
- const u = await ql.findOne(tableName, { where: { email } });
4952
- row = u?.value ?? u;
4953
- if (row) break;
4954
- } catch {
4955
- }
4956
- }
4957
- if (!row?.id) {
4958
- return { handled: true, response: this.error(`No user found with email '${email}'`, 404) };
4959
- }
4960
- inviteUserId = String(row.id);
4961
- }
4962
- const existing = await findOne(MEM, { environment_id: id, user_id: inviteUserId });
4963
- if (existing) {
4964
- return { handled: true, response: this.success({ member: existing, alreadyMember: true }) };
4965
- }
4966
- try {
4967
- const memberId = randomUUID();
4968
- await ql.insert(MEM, {
4969
- id: memberId,
4970
- environment_id: id,
4971
- user_id: inviteUserId,
4972
- role,
4973
- invited_by: callerId,
4974
- organization_id: project.organization_id ?? null
4975
- });
4976
- const created = await findOne(MEM, { id: memberId });
4977
- return { handled: true, response: this.success({ member: created, alreadyMember: false }) };
4978
- } catch (e) {
4979
- return { handled: true, response: this.error(e?.message ?? "Failed to add member", 500) };
4980
- }
4981
- }
4982
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "PATCH") {
4983
- const id = decodeURIComponent(parts[1]);
4984
- const memberId = decodeURIComponent(parts[3]);
4985
- const project = await findOne(ENV, { id });
4986
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
4987
- const callerId = await this.resolveCallerUserId(_context);
4988
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
4989
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
4990
- if (!callerMem || !["owner", "admin"].includes(String(callerMem.role))) {
4991
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
4992
- }
4993
- const target = await findOne(MEM, { id: memberId, environment_id: id });
4994
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
4995
- const newRole = String(body?.role ?? "").trim().toLowerCase();
4996
- if (!["owner", "admin", "member", "viewer"].includes(newRole)) {
4997
- return { handled: true, response: this.error(`Invalid role '${newRole}'`, 400) };
4998
- }
4999
- if (target.role === "owner" && newRole !== "owner") {
5000
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
5001
- if (owners && owners.value) owners = owners.value;
5002
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
5003
- if (ownerCount <= 1) {
5004
- return { handled: true, response: this.error("Cannot demote the last owner", 409) };
5005
- }
5006
- }
5007
- try {
5008
- await ql.update(MEM, { role: newRole, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, { where: { id: memberId } });
5009
- const updated = await findOne(MEM, { id: memberId });
5010
- return { handled: true, response: this.success({ member: updated }) };
5011
- } catch (e) {
5012
- return { handled: true, response: this.error(e?.message ?? "Failed to update role", 500) };
5013
- }
5014
- }
5015
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "members" && m === "DELETE") {
5016
- const id = decodeURIComponent(parts[1]);
5017
- const memberId = decodeURIComponent(parts[3]);
5018
- const project = await findOne(ENV, { id });
5019
- if (!project) return { handled: true, response: this.error(`Project '${id}' not found`, 404) };
5020
- const callerId = await this.resolveCallerUserId(_context);
5021
- if (!callerId) return { handled: true, response: this.error("Authentication required", 401) };
5022
- const target = await findOne(MEM, { id: memberId, environment_id: id });
5023
- if (!target) return { handled: true, response: this.error(`Member '${memberId}' not found`, 404) };
5024
- const callerMem = await findOne(MEM, { environment_id: id, user_id: callerId });
5025
- const isSelf = String(target.user_id) === String(callerId);
5026
- const isPrivileged = callerMem && ["owner", "admin"].includes(String(callerMem.role));
5027
- if (!isSelf && !isPrivileged) {
5028
- return { handled: true, response: this.error("Forbidden \u2014 owner or admin required", 403) };
5029
- }
5030
- if (target.role === "owner") {
5031
- let owners = await ql.find(MEM, { where: { environment_id: id, role: "owner" } });
5032
- if (owners && owners.value) owners = owners.value;
5033
- const ownerCount = Array.isArray(owners) ? owners.length : 0;
5034
- if (ownerCount <= 1) {
5035
- return { handled: true, response: this.error("Cannot remove the last owner", 409) };
5036
- }
5037
- }
5038
- try {
5039
- await ql.delete(MEM, { where: { id: memberId } });
5040
- return { handled: true, response: this.success({ removed: true, memberId }) };
5041
- } catch (e) {
5042
- return { handled: true, response: this.error(e?.message ?? "Failed to remove member", 500) };
5043
- }
5044
- }
5045
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
5046
- const envId = decodeURIComponent(parts[1]);
5047
- let rows = await ql.find(PKG_INSTALL, { where: { environment_id: envId } });
5048
- if (rows && rows.value) rows = rows.value;
5049
- const installs = Array.isArray(rows) ? rows : [];
5050
- const packages = await Promise.all(
5051
- installs.map(async (r) => {
5052
- let manifestId = null;
5053
- let versionStr = null;
5054
- try {
5055
- if (r.package_id) {
5056
- const pkg = await ql.findOne(PKG, { where: { id: r.package_id } });
5057
- manifestId = pkg?.manifest_id ?? null;
5058
- }
5059
- if (r.package_version_id) {
5060
- const ver = await ql.findOne(PKG_VERSION, { where: { id: r.package_version_id } });
5061
- versionStr = ver?.version ?? null;
5062
- }
5063
- } catch {
5064
- }
5065
- return {
5066
- ...r,
5067
- // Surface user-facing identifiers expected by client SDK
5068
- packageId: manifestId,
5069
- package_id: manifestId ?? r.package_id,
5070
- version: versionStr ?? r.version ?? null
5071
- };
5072
- })
5073
- );
5074
- return { handled: true, response: this.success({ packages, total: packages.length }) };
5075
- }
5076
- if (parts.length === 3 && parts[0] === "projects" && parts[2] === "packages" && m === "POST") {
5077
- const envId = decodeURIComponent(parts[1]);
5078
- const { packageId, version, settings, enableOnInstall } = body ?? {};
5079
- if (!packageId) return { handled: true, response: this.error("packageId is required", 400) };
5080
- const qlSvc = await this.getObjectQLService();
5081
- const pkgRegistry = qlSvc?.registry;
5082
- const allPkgs = pkgRegistry?.getAllPackages?.() ?? [];
5083
- const manifestEntry = allPkgs.find((p) => (p?.manifest?.id ?? p?.id) === packageId);
5084
- const manifest = manifestEntry?.manifest ?? manifestEntry;
5085
- if (!manifest) {
5086
- return { handled: true, response: this.error(`Package '${packageId}' is not registered on this server`, 404) };
5087
- }
5088
- const CLOUD_SCOPES = /* @__PURE__ */ new Set(["cloud", "system", "platform"]);
5089
- if (CLOUD_SCOPES.has(manifest?.scope)) {
5090
- return { handled: true, response: this.error(`Package '${packageId}' has scope=${manifest.scope} and cannot be installed per-project`, 403) };
5091
- }
5092
- const projectRow = await findOne(ENV, { id: envId });
5093
- if (!projectRow) {
5094
- return { handled: true, response: this.error(`Project '${envId}' not found`, 404) };
5095
- }
5096
- const ownerOrgId = projectRow.organization_id ?? "system";
5097
- let userId = "system";
5098
- try {
5099
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
5100
- const sessionData = await authService?.api?.getSession?.({
5101
- headers: _context?.request?.headers
5102
- });
5103
- userId = sessionData?.user?.id ?? sessionData?.session?.userId ?? "system";
5104
- } catch {
5105
- }
5106
- const resolvedVersion = version ?? manifest?.version ?? "1.0.0";
5107
- const dup = await ql.findOne(PKG_INSTALL, {
5108
- where: { environment_id: envId, package_id: packageId }
5109
- });
5110
- if (dup?.id) {
5111
- return { handled: true, response: this.error(`Package '${packageId}' is already installed in this project`, 409) };
5112
- }
5113
- const sysPackageId = await ensureSysPackage(packageId, ownerOrgId, userId, manifest);
5114
- const sysPackageVersionId = await ensureSysPackageVersion(sysPackageId, resolvedVersion, userId, manifest);
5115
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5116
- const recordId = randomUUID();
5117
- await ql.insert(PKG_INSTALL, {
5118
- id: recordId,
5119
- environment_id: envId,
5120
- package_id: sysPackageId,
5121
- package_version_id: sysPackageVersionId,
5122
- status: "installed",
5123
- enabled: enableOnInstall !== false,
5124
- installed_at: nowIso,
5125
- installed_by: userId,
5126
- updated_at: nowIso,
5127
- settings: settings ? JSON.stringify(settings) : null
5128
- });
5129
- const record = await ql.findOne(PKG_INSTALL, { where: { id: recordId } });
5130
- try {
5131
- await this.kernelManager?.evict(envId);
5132
- } catch {
5133
- }
5134
- return { handled: true, response: this.success({ package: record }) };
5135
- }
5136
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "GET") {
5137
- const envId = decodeURIComponent(parts[1]);
5138
- const pkgId = decodeURIComponent(parts[3]);
5139
- const record = await ql.findOne(PKG_INSTALL, { where: { environment_id: envId, package_id: pkgId } });
5140
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5141
- return { handled: true, response: this.success({ package: record }) };
5142
- }
5143
- if (parts.length === 4 && parts[0] === "projects" && parts[2] === "packages" && m === "DELETE") {
5144
- const envId = decodeURIComponent(parts[1]);
5145
- const pkgId = decodeURIComponent(parts[3]);
5146
- const record = await findInstallByManifestId(envId, pkgId);
5147
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5148
- const allPkgs0 = this.kernel.packages?.getAll?.() ?? [];
5149
- const m0 = allPkgs0.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5150
- if (m0?.scope && ["cloud", "system", "platform"].includes(m0.scope)) {
5151
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m0.scope} cannot be uninstalled`, 403) };
5152
- }
5153
- await ql.delete(PKG_INSTALL, { where: { id: record.id } });
5154
- try {
5155
- await this.kernelManager?.evict(envId);
5156
- } catch {
5157
- }
5158
- return { handled: true, response: this.success({ id: record.id, success: true }) };
5159
- }
5160
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "enable" && m === "PATCH") {
5161
- const envId = decodeURIComponent(parts[1]);
5162
- const pkgId = decodeURIComponent(parts[3]);
5163
- const record = await findInstallByManifestId(envId, pkgId);
5164
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5165
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5166
- await ql.update(PKG_INSTALL, { enabled: true, status: "installed", updated_at: nowIso }, { where: { id: record.id } });
5167
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5168
- try {
5169
- await this.kernelManager?.evict(envId);
5170
- } catch {
5171
- }
5172
- return { handled: true, response: this.success({ package: updated }) };
5173
- }
5174
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "disable" && m === "PATCH") {
5175
- const envId = decodeURIComponent(parts[1]);
5176
- const pkgId = decodeURIComponent(parts[3]);
5177
- const record = await findInstallByManifestId(envId, pkgId);
5178
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5179
- const allPkgs1 = this.kernel.packages?.getAll?.() ?? [];
5180
- const m1 = allPkgs1.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5181
- if (m1?.scope && ["cloud", "system", "platform"].includes(m1.scope)) {
5182
- return { handled: true, response: this.error(`Package '${pkgId}' with scope=${m1.scope} cannot be disabled`, 403) };
5183
- }
5184
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5185
- await ql.update(PKG_INSTALL, { enabled: false, status: "disabled", updated_at: nowIso }, { where: { id: record.id } });
5186
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5187
- try {
5188
- await this.kernelManager?.evict(envId);
5189
- } catch {
5190
- }
5191
- return { handled: true, response: this.success({ package: updated }) };
5192
- }
5193
- if (parts.length === 5 && parts[0] === "projects" && parts[2] === "packages" && parts[4] === "upgrade" && m === "POST") {
5194
- const envId = decodeURIComponent(parts[1]);
5195
- const pkgId = decodeURIComponent(parts[3]);
5196
- const record = await findInstallByManifestId(envId, pkgId);
5197
- if (!record) return { handled: true, response: this.error(`Package '${pkgId}' is not installed in this project`, 404) };
5198
- const { targetVersion } = body ?? {};
5199
- const allPkgs2 = this.kernel.packages?.getAll?.() ?? [];
5200
- const manifest2 = allPkgs2.find((p) => (p.manifest?.id ?? p.id) === pkgId)?.manifest;
5201
- const currentVer = await ql.findOne(PKG_VERSION, { where: { id: record.package_version_id } });
5202
- const newVersion = targetVersion ?? manifest2?.version ?? currentVer?.version ?? "1.0.0";
5203
- if (newVersion === currentVer?.version) {
5204
- return { handled: true, response: this.success({ package: record, message: "Already at target version" }) };
5205
- }
5206
- let userId = "system";
5207
- try {
5208
- const authService = await this.getService(import_system.CoreServiceName.enum.auth);
5209
- const sessionData = await authService?.api?.getSession?.({
5210
- headers: _context?.request?.headers
5211
- });
5212
- userId = sessionData?.user?.id ?? "system";
5213
- } catch {
5214
- }
5215
- const newVersionId = await ensureSysPackageVersion(record.package_id, newVersion, userId, manifest2);
5216
- const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5217
- await ql.update(PKG_INSTALL, {
5218
- package_version_id: newVersionId,
5219
- status: "installed",
5220
- updated_at: nowIso
5221
- }, { where: { id: record.id } });
5222
- const updated = await ql.findOne(PKG_INSTALL, { where: { id: record.id } });
5223
- try {
5224
- await this.kernelManager?.evict(envId);
5225
- } catch {
5226
- }
5227
- return { handled: true, response: this.success({ package: updated }) };
5228
- }
5229
- } catch (e) {
5230
- return { handled: true, response: this.error(e.message, e.statusCode || 500) };
5231
- }
5232
- return { handled: false };
5233
- }
5234
- /**
5235
- * Cascade-delete a project: cred / member / package_installation rows,
5236
- * then the physical database via the provisioning adapter, then the
5237
- * `sys_environment` row itself. Used by both `DELETE /cloud/environments/:id`
5238
- * and the org-cascade in `DELETE /cloud/organizations/:id`.
5239
- *
5240
- * Idempotent and best-effort: missing rows / unreachable adapters
5241
- * become warnings rather than hard failures, so a half-provisioned
5242
- * project can still be cleaned out.
5243
- */
5244
- async deleteProjectCascade(environmentId, deps) {
5245
- const { ql, findOne, getRealAdapter, force } = deps;
5246
- const ENV = "sys_environment";
5247
- const warnings = [];
5248
- const row = await findOne(ENV, { id: environmentId });
5249
- if (!row) {
5250
- return { ok: false, status: 404, error: `Project '${environmentId}' not found`, warnings };
5251
- }
5252
- if (row.is_system === true || row.is_system === 1) {
5253
- return { ok: false, status: 409, error: `Project '${environmentId}' is a system project and cannot be deleted`, warnings };
5254
- }
5255
- if ((row.is_default === true || row.is_default === 1) && !force) {
5256
- return {
5257
- ok: false,
5258
- status: 409,
5259
- error: `Project '${environmentId}' is the default project for its organization. Pass ?force=1 to delete it.`,
5260
- warnings
5261
- };
5262
- }
5263
- const cascade = [
5264
- { object: "sys_environment_credential", field: "environment_id" },
5265
- { object: "sys_environment_member", field: "environment_id" },
5266
- { object: "sys_package_installation", field: "environment_id" }
5267
- ];
5268
- for (const { object, field } of cascade) {
5269
- try {
5270
- let rows = await ql.find(object, { where: { [field]: environmentId } });
5271
- if (rows && rows.value) rows = rows.value;
5272
- if (Array.isArray(rows)) {
5273
- for (const r of rows) {
5274
- if (r?.id != null) {
5275
- try {
5276
- await ql.delete(object, { where: { id: r.id } });
5277
- } catch (innerErr) {
5278
- warnings.push(
5279
- `Failed to delete ${object} ${r.id}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
5280
- );
5281
- }
5282
- }
5283
- }
5284
- }
5285
- } catch (err) {
5286
- warnings.push(
5287
- `Failed to enumerate ${object} for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5288
- );
5289
- }
5290
- }
5291
- const driver = row.database_driver ?? "memory";
5292
- const databaseUrl = row.database_url;
5293
- const databaseName = `p-${String(environmentId).replace(/-/g, "").slice(0, 24)}`;
5294
- try {
5295
- const adapter = await getRealAdapter(driver);
5296
- if (adapter?.deleteDatabase) {
5297
- await adapter.deleteDatabase({ environmentId, databaseName, databaseUrl });
5298
- } else {
5299
- warnings.push(`No adapter for driver '${driver}'; physical DB for project ${environmentId} not released.`);
5300
- }
5301
- } catch (err) {
5302
- warnings.push(
5303
- `Failed to delete physical database for project ${environmentId}: ${err instanceof Error ? err.message : String(err)}`
5304
- );
5305
- }
5306
- try {
5307
- await ql.delete(ENV, { where: { id: environmentId } });
5308
- } catch (err) {
5309
- return {
5310
- ok: false,
5311
- status: 500,
5312
- error: `Failed to delete sys_environment row: ${err instanceof Error ? err.message : String(err)}`,
5313
- warnings
5314
- };
5315
- }
5316
- if (this.envRegistry?.invalidate) {
5317
- try {
5318
- await this.envRegistry.invalidate(environmentId);
5319
- } catch {
5320
- }
5321
- }
5322
- return { ok: true, warnings };
5323
- }
5324
- /**
5325
- * Handles Storage requests
5326
- * path: sub-path after /storage/
5327
- */
5328
- async handleStorage(path, method, file, context) {
5329
- const storageService = await this.getService(import_system.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
5330
- if (!storageService) {
5331
- return { handled: true, response: this.error("File storage not configured", 501) };
4083
+ /**
4084
+ * Handles Storage requests
4085
+ * path: sub-path after /storage/
4086
+ */
4087
+ async handleStorage(path, method, file, context) {
4088
+ const storageService = await this.getService(import_system2.CoreServiceName.enum["file-storage"]) || this.kernel.services?.["file-storage"];
4089
+ if (!storageService) {
4090
+ return { handled: true, response: this.error("File storage not configured", 501) };
5332
4091
  }
5333
4092
  const m = method.toUpperCase();
5334
4093
  const parts = path.replace(/^\/+/, "").split("/");
@@ -5391,6 +4150,8 @@ var _HttpDispatcher = class _HttpDispatcher {
5391
4150
  *
5392
4151
  * Routes:
5393
4152
  * GET / → listFlows
4153
+ * GET /actions → getActionDescriptors (ADR-0018; ?paradigm/?source/?category filters)
4154
+ * GET /connectors → getConnectorDescriptors (ADR-0022; ?type filter)
5394
4155
  * GET /:name → getFlow
5395
4156
  * POST / → createFlow (registerFlow)
5396
4157
  * PUT /:name → updateFlow
@@ -5399,9 +4160,11 @@ var _HttpDispatcher = class _HttpDispatcher {
5399
4160
  * POST /:name/toggle → toggleFlow
5400
4161
  * GET /:name/runs → listRuns
5401
4162
  * GET /:name/runs/:runId → getRun
4163
+ * POST /:name/runs/:runId/resume → resume a paused run (screen input / ADR-0019)
4164
+ * GET /:name/runs/:runId/screen → the screen a paused run awaits
5402
4165
  */
5403
4166
  async handleAutomation(path, method, body, context, query) {
5404
- const automationService = await this.getService(import_system.CoreServiceName.enum.automation);
4167
+ const automationService = await this.getService(import_system2.CoreServiceName.enum.automation);
5405
4168
  if (!automationService) return { handled: false };
5406
4169
  const m = method.toUpperCase();
5407
4170
  const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -5428,6 +4191,32 @@ var _HttpDispatcher = class _HttpDispatcher {
5428
4191
  return { handled: true, response: this.success(body) };
5429
4192
  }
5430
4193
  }
4194
+ if (parts[0] === "actions" && parts.length === 1 && m === "GET") {
4195
+ if (typeof automationService.getActionDescriptors === "function") {
4196
+ let actions = automationService.getActionDescriptors() ?? [];
4197
+ if (query?.paradigm) {
4198
+ actions = actions.filter((a) => Array.isArray(a?.paradigms) && a.paradigms.includes(query.paradigm));
4199
+ }
4200
+ if (query?.source) {
4201
+ actions = actions.filter((a) => a?.source === query.source);
4202
+ }
4203
+ if (query?.category) {
4204
+ actions = actions.filter((a) => a?.category === query.category);
4205
+ }
4206
+ return { handled: true, response: this.success({ actions, total: actions.length }) };
4207
+ }
4208
+ return { handled: true, response: this.success({ actions: [], total: 0 }) };
4209
+ }
4210
+ if (parts[0] === "connectors" && parts.length === 1 && m === "GET") {
4211
+ if (typeof automationService.getConnectorDescriptors === "function") {
4212
+ let connectors = automationService.getConnectorDescriptors() ?? [];
4213
+ if (query?.type) {
4214
+ connectors = connectors.filter((c) => c?.type === query.type);
4215
+ }
4216
+ return { handled: true, response: this.success({ connectors, total: connectors.length }) };
4217
+ }
4218
+ return { handled: true, response: this.success({ connectors: [], total: 0 }) };
4219
+ }
5431
4220
  if (parts.length >= 1) {
5432
4221
  const name = parts[0];
5433
4222
  if (parts[1] === "trigger" && m === "POST") {
@@ -5467,7 +4256,28 @@ var _HttpDispatcher = class _HttpDispatcher {
5467
4256
  return { handled: true, response: this.success({ name, enabled: body?.enabled ?? true }) };
5468
4257
  }
5469
4258
  }
5470
- if (parts[1] === "runs" && parts[2] && m === "GET") {
4259
+ if (parts[1] === "runs" && parts[2] && parts[3] === "resume" && m === "POST") {
4260
+ if (typeof automationService.resume === "function") {
4261
+ const b = body && typeof body === "object" ? body : {};
4262
+ const inputs = b.inputs ?? b.variables;
4263
+ const signal = {};
4264
+ if (inputs && typeof inputs === "object") signal.variables = inputs;
4265
+ if (b.output && typeof b.output === "object") signal.output = b.output;
4266
+ if (typeof b.branchLabel === "string") signal.branchLabel = b.branchLabel;
4267
+ const result = await automationService.resume(parts[2], signal);
4268
+ return { handled: true, response: this.success(result) };
4269
+ }
4270
+ return { handled: true, response: this.error("Resume not supported", 501) };
4271
+ }
4272
+ if (parts[1] === "runs" && parts[2] && parts[3] === "screen" && m === "GET") {
4273
+ if (typeof automationService.getSuspendedScreen === "function") {
4274
+ const screen = automationService.getSuspendedScreen(parts[2]);
4275
+ if (!screen) return { handled: true, response: this.error("No pending screen for run", 404) };
4276
+ return { handled: true, response: this.success({ runId: parts[2], screen }) };
4277
+ }
4278
+ return { handled: true, response: this.error("Screen lookup not supported", 501) };
4279
+ }
4280
+ if (parts[1] === "runs" && parts[2] && !parts[3] && m === "GET") {
5471
4281
  if (typeof automationService.getRun === "function") {
5472
4282
  const run = await automationService.getRun(parts[2]);
5473
4283
  if (!run) return { handled: true, response: this.error("Execution not found", 404) };
@@ -5793,11 +4603,9 @@ var _HttpDispatcher = class _HttpDispatcher {
5793
4603
  if (forbidden) {
5794
4604
  return { handled: true, response: forbidden };
5795
4605
  }
5796
- if (!cleanPath.startsWith("/cloud/")) {
5797
- const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
5798
- if (scopedMatch) {
5799
- cleanPath = scopedMatch[1] ?? "";
5800
- }
4606
+ const scopedMatch = cleanPath.match(/^\/projects\/[^/]+(\/.*)?$/);
4607
+ if (scopedMatch) {
4608
+ cleanPath = scopedMatch[1] ?? "";
5801
4609
  }
5802
4610
  try {
5803
4611
  if ((cleanPath === "/discovery" || cleanPath === "") && method === "GET") {
@@ -5848,9 +4656,6 @@ var _HttpDispatcher = class _HttpDispatcher {
5848
4656
  if (cleanPath.startsWith("/packages")) {
5849
4657
  return this.handlePackages(cleanPath.substring(9), method, body, query, context);
5850
4658
  }
5851
- if (cleanPath.startsWith("/cloud")) {
5852
- return this.handleCloud(cleanPath.substring(6), method, body, query, context);
5853
- }
5854
4659
  if (cleanPath.startsWith("/i18n")) {
5855
4660
  return this.handleI18n(cleanPath.substring(5), method, query, context);
5856
4661
  }
@@ -6466,344 +5271,136 @@ function createDispatcherPlugin(config = {}) {
6466
5271
  res.header(k, v);
6467
5272
  }
6468
5273
  }
6469
- res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6470
- });
6471
- server.get(`${prefix}/discovery`, async (_req, res) => {
6472
- if (securityHeaders) {
6473
- for (const [k, v] of Object.entries(securityHeaders)) {
6474
- res.header(k, v);
6475
- }
6476
- }
6477
- res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6478
- });
6479
- server.get(`${prefix}/health`, async (_req, res) => {
6480
- try {
6481
- const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
6482
- sendResult(result, res);
6483
- } catch (err) {
6484
- errorResponse(err, res);
6485
- }
6486
- });
6487
- server.post(`${prefix}/auth/login`, async (req, res) => {
6488
- try {
6489
- const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
6490
- sendResult(result, res);
6491
- } catch (err) {
6492
- errorResponse(err, res);
6493
- }
6494
- });
6495
- server.post(`${prefix}/graphql`, async (req, res) => {
6496
- try {
6497
- const result = await dispatcher.handleGraphQL(req.body, { request: req });
6498
- if (securityHeaders) {
6499
- for (const [k, v] of Object.entries(securityHeaders)) {
6500
- res.header(k, v);
6501
- }
6502
- }
6503
- res.json(result);
6504
- } catch (err) {
6505
- errorResponse(err, res);
6506
- }
6507
- });
6508
- server.post(`${prefix}/analytics/query`, async (req, res) => {
6509
- try {
6510
- const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
6511
- sendResult(result, res);
6512
- } catch (err) {
6513
- errorResponse(err, res);
6514
- }
6515
- });
6516
- server.get(`${prefix}/analytics/meta`, async (req, res) => {
6517
- try {
6518
- const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
6519
- sendResult(result, res);
6520
- } catch (err) {
6521
- errorResponse(err, res);
6522
- }
6523
- });
6524
- server.post(`${prefix}/analytics/sql`, async (req, res) => {
6525
- try {
6526
- const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
6527
- sendResult(result, res);
6528
- } catch (err) {
6529
- errorResponse(err, res);
6530
- }
6531
- });
6532
- server.get(`${prefix}/packages`, async (req, res) => {
6533
- try {
6534
- const result = await dispatcher.handlePackages("", "GET", {}, req.query, { request: req });
6535
- sendResult(result, res);
6536
- } catch (err) {
6537
- errorResponse(err, res);
6538
- }
6539
- });
6540
- server.post(`${prefix}/packages`, async (req, res) => {
6541
- try {
6542
- const result = await dispatcher.handlePackages("", "POST", req.body, {}, { request: req });
6543
- sendResult(result, res);
6544
- } catch (err) {
6545
- errorResponse(err, res);
6546
- }
6547
- });
6548
- server.get(`${prefix}/packages/:id/export`, async (req, res) => {
6549
- try {
6550
- const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
6551
- sendResult(result, res);
6552
- } catch (err) {
6553
- errorResponse(err, res);
6554
- }
6555
- });
6556
- server.get(`${prefix}/packages/:id`, async (req, res) => {
6557
- try {
6558
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6559
- sendResult(result, res);
6560
- } catch (err) {
6561
- errorResponse(err, res);
6562
- }
6563
- });
6564
- server.delete(`${prefix}/packages/:id`, async (req, res) => {
6565
- try {
6566
- const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6567
- sendResult(result, res);
6568
- } catch (err) {
6569
- errorResponse(err, res);
6570
- }
6571
- });
6572
- server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6573
- try {
6574
- const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6575
- sendResult(result, res);
6576
- } catch (err) {
6577
- errorResponse(err, res);
6578
- }
6579
- });
6580
- server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6581
- try {
6582
- const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6583
- sendResult(result, res);
6584
- } catch (err) {
6585
- errorResponse(err, res);
6586
- }
6587
- });
6588
- server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6589
- try {
6590
- const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6591
- sendResult(result, res);
6592
- } catch (err) {
6593
- errorResponse(err, res);
6594
- }
6595
- });
6596
- server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6597
- try {
6598
- const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6599
- sendResult(result, res);
6600
- } catch (err) {
6601
- errorResponse(err, res);
6602
- }
6603
- });
6604
- server.get(`${prefix}/cloud/drivers`, async (req, res) => {
6605
- try {
6606
- const result = await dispatcher.handleCloud("/drivers", "GET", {}, req.query, { request: req });
6607
- sendResult(result, res);
6608
- } catch (err) {
6609
- errorResponse(err, res);
6610
- }
6611
- });
6612
- server.post(`${prefix}/cloud/admin/platform-sso/backfill`, async (req, res) => {
6613
- try {
6614
- const result = await dispatcher.handleCloud("/admin/platform-sso/backfill", "POST", req.body, req.query, { request: req });
6615
- sendResult(result, res);
6616
- } catch (err) {
6617
- errorResponse(err, res);
6618
- }
6619
- });
6620
- server.get(`${prefix}/cloud/templates`, async (req, res) => {
6621
- try {
6622
- const result = await dispatcher.handleCloud("/templates", "GET", {}, req.query, { request: req });
6623
- sendResult(result, res);
6624
- } catch (err) {
6625
- errorResponse(err, res);
6626
- }
6627
- });
6628
- server.get(`${prefix}/cloud/environments`, async (req, res) => {
6629
- try {
6630
- const result = await dispatcher.handleCloud("/projects", "GET", {}, req.query, { request: req });
6631
- sendResult(result, res);
6632
- } catch (err) {
6633
- errorResponse(err, res);
6634
- }
6635
- });
6636
- server.post(`${prefix}/cloud/environments`, async (req, res) => {
6637
- try {
6638
- const result = await dispatcher.handleCloud("/projects", "POST", req.body, {}, { request: req });
6639
- sendResult(result, res);
6640
- } catch (err) {
6641
- errorResponse(err, res);
6642
- }
6643
- });
6644
- server.get(`${prefix}/cloud/environments/:id`, async (req, res) => {
6645
- try {
6646
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "GET", {}, req.query, { request: req });
6647
- sendResult(result, res);
6648
- } catch (err) {
6649
- errorResponse(err, res);
6650
- }
6651
- });
6652
- server.patch(`${prefix}/cloud/environments/:id`, async (req, res) => {
6653
- try {
6654
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "PATCH", req.body, {}, { request: req });
6655
- sendResult(result, res);
6656
- } catch (err) {
6657
- errorResponse(err, res);
6658
- }
6659
- });
6660
- server.delete(`${prefix}/cloud/environments/:id`, async (req, res) => {
6661
- try {
6662
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6663
- sendResult(result, res);
6664
- } catch (err) {
6665
- errorResponse(err, res);
6666
- }
6667
- });
6668
- server.delete(`${prefix}/cloud/organizations/:id`, async (req, res) => {
6669
- try {
6670
- const result = await dispatcher.handleCloud(`/organizations/${req.params.id}`, "DELETE", {}, req.query, { request: req });
6671
- sendResult(result, res);
6672
- } catch (err) {
6673
- errorResponse(err, res);
6674
- }
6675
- });
6676
- server.post(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6677
- try {
6678
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "POST", req.body, {}, { request: req });
6679
- sendResult(result, res);
6680
- } catch (err) {
6681
- errorResponse(err, res);
6682
- }
6683
- });
6684
- server.put(`${prefix}/cloud/environments/:id/hostname`, async (req, res) => {
6685
- try {
6686
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/hostname`, "PUT", req.body, {}, { request: req });
6687
- sendResult(result, res);
6688
- } catch (err) {
6689
- errorResponse(err, res);
6690
- }
5274
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
5275
+ });
5276
+ server.get(`${prefix}/discovery`, async (_req, res) => {
5277
+ if (securityHeaders) {
5278
+ for (const [k, v] of Object.entries(securityHeaders)) {
5279
+ res.header(k, v);
5280
+ }
5281
+ }
5282
+ res.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
6691
5283
  });
6692
- server.post(`${prefix}/cloud/environments/:id/rotate-credential`, async (req, res) => {
5284
+ server.get(`${prefix}/health`, async (_req, res) => {
6693
5285
  try {
6694
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/rotate-credential`, "POST", req.body, {}, { request: req });
5286
+ const result = await dispatcher.dispatch("GET", "/health", void 0, {}, { request: _req });
6695
5287
  sendResult(result, res);
6696
5288
  } catch (err) {
6697
5289
  errorResponse(err, res);
6698
5290
  }
6699
5291
  });
6700
- server.post(`${prefix}/cloud/environments/:id/credentials/rotate`, async (req, res) => {
5292
+ server.post(`${prefix}/auth/login`, async (req, res) => {
6701
5293
  try {
6702
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/credentials/rotate`, "POST", req.body, {}, { request: req });
5294
+ const result = await dispatcher.handleAuth("login", "POST", req.body, { request: req });
6703
5295
  sendResult(result, res);
6704
5296
  } catch (err) {
6705
5297
  errorResponse(err, res);
6706
5298
  }
6707
5299
  });
6708
- server.post(`${prefix}/cloud/environments/:id/activate`, async (req, res) => {
5300
+ server.post(`${prefix}/graphql`, async (req, res) => {
6709
5301
  try {
6710
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/activate`, "POST", req.body, {}, { request: req });
6711
- sendResult(result, res);
5302
+ const result = await dispatcher.handleGraphQL(req.body, { request: req });
5303
+ if (securityHeaders) {
5304
+ for (const [k, v] of Object.entries(securityHeaders)) {
5305
+ res.header(k, v);
5306
+ }
5307
+ }
5308
+ res.json(result);
6712
5309
  } catch (err) {
6713
5310
  errorResponse(err, res);
6714
5311
  }
6715
5312
  });
6716
- server.post(`${prefix}/cloud/environments/:id/retry`, async (req, res) => {
5313
+ server.post(`${prefix}/analytics/query`, async (req, res) => {
6717
5314
  try {
6718
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/retry`, "POST", req.body, {}, { request: req });
5315
+ const result = await dispatcher.dispatch("POST", "/analytics/query", req.body, req.query, { request: req });
6719
5316
  sendResult(result, res);
6720
5317
  } catch (err) {
6721
5318
  errorResponse(err, res);
6722
5319
  }
6723
5320
  });
6724
- server.get(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
5321
+ server.get(`${prefix}/analytics/meta`, async (req, res) => {
6725
5322
  try {
6726
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "GET", {}, req.query, { request: req });
5323
+ const result = await dispatcher.dispatch("GET", "/analytics/meta", void 0, req.query, { request: req });
6727
5324
  sendResult(result, res);
6728
5325
  } catch (err) {
6729
5326
  errorResponse(err, res);
6730
5327
  }
6731
5328
  });
6732
- server.post(`${prefix}/cloud/environments/:id/members`, async (req, res) => {
5329
+ server.post(`${prefix}/analytics/sql`, async (req, res) => {
6733
5330
  try {
6734
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members`, "POST", req.body, {}, { request: req });
5331
+ const result = await dispatcher.dispatch("POST", "/analytics/sql", req.body, req.query, { request: req });
6735
5332
  sendResult(result, res);
6736
5333
  } catch (err) {
6737
5334
  errorResponse(err, res);
6738
5335
  }
6739
5336
  });
6740
- server.patch(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
5337
+ server.get(`${prefix}/packages`, async (req, res) => {
6741
5338
  try {
6742
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "PATCH", req.body, {}, { request: req });
5339
+ const result = await dispatcher.handlePackages("", "GET", {}, req.query, { request: req });
6743
5340
  sendResult(result, res);
6744
5341
  } catch (err) {
6745
5342
  errorResponse(err, res);
6746
5343
  }
6747
5344
  });
6748
- server.delete(`${prefix}/cloud/environments/:id/members/:memberId`, async (req, res) => {
5345
+ server.post(`${prefix}/packages`, async (req, res) => {
6749
5346
  try {
6750
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/members/${req.params.memberId}`, "DELETE", req.body ?? {}, {}, { request: req });
5347
+ const result = await dispatcher.handlePackages("", "POST", req.body, {}, { request: req });
6751
5348
  sendResult(result, res);
6752
5349
  } catch (err) {
6753
5350
  errorResponse(err, res);
6754
5351
  }
6755
5352
  });
6756
- server.get(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5353
+ server.get(`${prefix}/packages/:id/export`, async (req, res) => {
6757
5354
  try {
6758
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "GET", {}, req.query, { request: req });
5355
+ const result = await dispatcher.handlePackages(`/${req.params.id}/export`, "GET", {}, req.query, { request: req });
6759
5356
  sendResult(result, res);
6760
5357
  } catch (err) {
6761
5358
  errorResponse(err, res);
6762
5359
  }
6763
5360
  });
6764
- server.post(`${prefix}/cloud/environments/:id/packages`, async (req, res) => {
5361
+ server.get(`${prefix}/packages/:id`, async (req, res) => {
6765
5362
  try {
6766
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages`, "POST", req.body, {}, { request: req });
5363
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "GET", {}, req.query, { request: req });
6767
5364
  sendResult(result, res);
6768
5365
  } catch (err) {
6769
5366
  errorResponse(err, res);
6770
5367
  }
6771
5368
  });
6772
- server.get(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5369
+ server.delete(`${prefix}/packages/:id`, async (req, res) => {
6773
5370
  try {
6774
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "GET", {}, req.query, { request: req });
5371
+ const result = await dispatcher.handlePackages(`/${req.params.id}`, "DELETE", {}, {}, { request: req });
6775
5372
  sendResult(result, res);
6776
5373
  } catch (err) {
6777
5374
  errorResponse(err, res);
6778
5375
  }
6779
5376
  });
6780
- server.delete(`${prefix}/cloud/environments/:id/packages/:pkgId`, async (req, res) => {
5377
+ server.patch(`${prefix}/packages/:id/enable`, async (req, res) => {
6781
5378
  try {
6782
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}`, "DELETE", {}, {}, { request: req });
5379
+ const result = await dispatcher.handlePackages(`/${req.params.id}/enable`, "PATCH", {}, {}, { request: req });
6783
5380
  sendResult(result, res);
6784
5381
  } catch (err) {
6785
5382
  errorResponse(err, res);
6786
5383
  }
6787
5384
  });
6788
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/enable`, async (req, res) => {
5385
+ server.patch(`${prefix}/packages/:id/disable`, async (req, res) => {
6789
5386
  try {
6790
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/enable`, "PATCH", {}, {}, { request: req });
5387
+ const result = await dispatcher.handlePackages(`/${req.params.id}/disable`, "PATCH", {}, {}, { request: req });
6791
5388
  sendResult(result, res);
6792
5389
  } catch (err) {
6793
5390
  errorResponse(err, res);
6794
5391
  }
6795
5392
  });
6796
- server.patch(`${prefix}/cloud/environments/:id/packages/:pkgId/disable`, async (req, res) => {
5393
+ server.post(`${prefix}/packages/:id/publish`, async (req, res) => {
6797
5394
  try {
6798
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/disable`, "PATCH", {}, {}, { request: req });
5395
+ const result = await dispatcher.handlePackages(`/${req.params.id}/publish`, "POST", req.body, {}, { request: req });
6799
5396
  sendResult(result, res);
6800
5397
  } catch (err) {
6801
5398
  errorResponse(err, res);
6802
5399
  }
6803
5400
  });
6804
- server.post(`${prefix}/cloud/environments/:id/packages/:pkgId/upgrade`, async (req, res) => {
5401
+ server.post(`${prefix}/packages/:id/revert`, async (req, res) => {
6805
5402
  try {
6806
- const result = await dispatcher.handleCloud(`/projects/${req.params.id}/packages/${req.params.pkgId}/upgrade`, "POST", req.body, {}, { request: req });
5403
+ const result = await dispatcher.handlePackages(`/${req.params.id}/revert`, "POST", req.body, {}, { request: req });
6807
5404
  sendResult(result, res);
6808
5405
  } catch (err) {
6809
5406
  errorResponse(err, res);
@@ -6930,6 +5527,22 @@ function createDispatcherPlugin(config = {}) {
6930
5527
  errorResponse(err, res);
6931
5528
  }
6932
5529
  });
5530
+ server.post(`${base}/automation/:name/runs/:runId/resume`, async (req, res) => {
5531
+ try {
5532
+ const result = await dispatcher.dispatch("POST", `/automation/${req.params.name}/runs/${req.params.runId}/resume`, req.body, req.query, { request: req });
5533
+ sendResult(result, res);
5534
+ } catch (err) {
5535
+ errorResponse(err, res);
5536
+ }
5537
+ });
5538
+ server.get(`${base}/automation/:name/runs/:runId/screen`, async (req, res) => {
5539
+ try {
5540
+ const result = await dispatcher.dispatch("GET", `/automation/${req.params.name}/runs/${req.params.runId}/screen`, void 0, req.query, { request: req });
5541
+ sendResult(result, res);
5542
+ } catch (err) {
5543
+ errorResponse(err, res);
5544
+ }
5545
+ });
6933
5546
  };
6934
5547
  const registerAIRoutes = (base) => {
6935
5548
  const wildcards = [
@@ -7870,21 +6483,17 @@ async function createDriver(driverType, databaseUrl, authToken) {
7870
6483
  // src/cloud/artifact-kernel-factory.ts
7871
6484
  var import_node_crypto2 = require("crypto");
7872
6485
  var import_core3 = require("@objectstack/core");
7873
- var import_types4 = require("@objectstack/types");
6486
+ var import_types3 = require("@objectstack/types");
7874
6487
  init_driver_plugin();
7875
6488
  init_app_plugin();
7876
6489
 
7877
6490
  // src/cloud/capability-loader.ts
7878
6491
  var CAPABILITY_PROVIDERS = {
7879
6492
  automation: {
6493
+ // Self-contained: AutomationServicePlugin seeds all built-in node
6494
+ // executors itself (ADR-0018), so no companion node-pack plugins.
7880
6495
  pkg: "@objectstack/service-automation",
7881
- export: "AutomationServicePlugin",
7882
- extras: [
7883
- { pkg: "@objectstack/service-automation", export: "CrudNodesPlugin" },
7884
- { pkg: "@objectstack/service-automation", export: "LogicNodesPlugin" },
7885
- { pkg: "@objectstack/service-automation", export: "HttpConnectorPlugin" },
7886
- { pkg: "@objectstack/service-automation", export: "ScreenNodesPlugin" }
7887
- ]
6496
+ export: "AutomationServicePlugin"
7888
6497
  },
7889
6498
  ai: {
7890
6499
  pkg: "@objectstack/service-ai",
@@ -7915,6 +6524,19 @@ var CAPABILITY_PROVIDERS = {
7915
6524
  pkg: "@objectstack/service-job",
7916
6525
  export: "JobServicePlugin"
7917
6526
  },
6527
+ messaging: {
6528
+ // Backs the `notify` flow node (ADR-0012): delivers to a user's
6529
+ // channels (inbox by default → `sys_inbox_message` rows).
6530
+ pkg: "@objectstack/service-messaging",
6531
+ export: "MessagingServicePlugin"
6532
+ },
6533
+ triggers: {
6534
+ // Concrete flow triggers — record-change (ObjectQL hooks) + schedule
6535
+ // (cron/interval via the job service; pair `triggers` with `job`).
6536
+ pkg: "@objectstack/plugin-trigger-record-change",
6537
+ export: "RecordChangeTriggerPlugin",
6538
+ extras: [{ pkg: "@objectstack/plugin-trigger-schedule", export: "ScheduleTriggerPlugin" }]
6539
+ },
7918
6540
  realtime: {
7919
6541
  pkg: "@objectstack/service-realtime",
7920
6542
  export: "RealtimeServicePlugin"
@@ -7932,7 +6554,11 @@ async function loadCapabilities(opts) {
7932
6554
  const { kernel, requires, bundle, environmentId } = opts;
7933
6555
  const logger = opts.logger ?? console;
7934
6556
  const installed = [];
7935
- for (const cap of requires) {
6557
+ const resolved = [...new Set(requires)];
6558
+ if (resolved.includes("audit") && !resolved.includes("messaging")) {
6559
+ resolved.push("messaging");
6560
+ }
6561
+ for (const cap of resolved) {
7936
6562
  const spec = CAPABILITY_PROVIDERS[cap];
7937
6563
  if (!spec) {
7938
6564
  continue;
@@ -7999,8 +6625,197 @@ async function loadCapabilities(opts) {
7999
6625
  return installed;
8000
6626
  }
8001
6627
 
6628
+ // src/cloud/platform-sso.ts
6629
+ var import_node_crypto = require("crypto");
6630
+ var PLATFORM_SSO_PROVIDER_ID = "objectstack-cloud";
6631
+ function derivePlatformSsoClientId(environmentId) {
6632
+ return `project_${environmentId}`;
6633
+ }
6634
+ function derivePlatformSsoClientSecret(baseSecret, environmentId) {
6635
+ return (0, import_node_crypto.createHmac)("sha256", baseSecret).update(`oauth-client:${environmentId}`).digest("hex");
6636
+ }
6637
+ function hashPlatformSsoClientSecret(plaintext) {
6638
+ return (0, import_node_crypto.createHash)("sha256").update(plaintext).digest("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
6639
+ }
6640
+ function buildPlatformSsoRedirectUri(hostname, basePath = "/api/v1/auth") {
6641
+ let host;
6642
+ if (hostname.startsWith("http://") || hostname.startsWith("https://")) {
6643
+ host = hostname;
6644
+ } else if (/(\.|^)localhost(:\d+)?$/i.test(hostname)) {
6645
+ const port = (process.env.OS_RUNTIME_PORT ?? "").trim();
6646
+ const hostWithPort = /:\d+$/.test(hostname) || !port ? hostname : `${hostname}:${port}`;
6647
+ host = `http://${hostWithPort}`;
6648
+ } else {
6649
+ host = `https://${hostname}`;
6650
+ }
6651
+ const trimmed = host.replace(/\/+$/, "");
6652
+ const path = basePath.replace(/\/+$/, "");
6653
+ return `${trimmed}${path}/oauth2/callback/${PLATFORM_SSO_PROVIDER_ID}`;
6654
+ }
6655
+ async function seedPlatformSsoClient(opts) {
6656
+ const { ql, environmentId, hostname, baseSecret, logger, throwOnError } = opts;
6657
+ if (!baseSecret) {
6658
+ logger?.warn?.("[platform-sso] OS_AUTH_SECRET not set \u2014 skipping client seed", { environmentId });
6659
+ return;
6660
+ }
6661
+ const clientId = derivePlatformSsoClientId(environmentId);
6662
+ const clientSecretPlaintext = derivePlatformSsoClientSecret(baseSecret, environmentId);
6663
+ const clientSecretStored = hashPlatformSsoClientSecret(clientSecretPlaintext);
6664
+ const desiredRedirect = hostname ? buildPlatformSsoRedirectUri(hostname) : null;
6665
+ let existing = null;
6666
+ try {
6667
+ const rows = await ql.find("sys_oauth_application", {
6668
+ where: { client_id: clientId },
6669
+ limit: 1
6670
+ }, { context: { isSystem: true } });
6671
+ const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6672
+ existing = list[0] ?? null;
6673
+ } catch (err) {
6674
+ logger?.warn?.("[platform-sso] sys_oauth_application read failed \u2014 skipping seed", {
6675
+ environmentId,
6676
+ error: err?.message
6677
+ });
6678
+ return;
6679
+ }
6680
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6681
+ if (!existing) {
6682
+ const redirects = desiredRedirect ? [desiredRedirect] : [];
6683
+ try {
6684
+ await ql.insert("sys_oauth_application", {
6685
+ id: `oauthc_${environmentId}`,
6686
+ name: `Project ${environmentId}`,
6687
+ client_id: clientId,
6688
+ client_secret: clientSecretStored,
6689
+ type: "web",
6690
+ redirect_uris: JSON.stringify(redirects),
6691
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6692
+ response_types: JSON.stringify(["code"]),
6693
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6694
+ token_endpoint_auth_method: "client_secret_basic",
6695
+ require_pkce: false,
6696
+ skip_consent: true,
6697
+ disabled: false,
6698
+ subject_type: "public",
6699
+ created_at: nowIso,
6700
+ updated_at: nowIso
6701
+ }, { context: { isSystem: true } });
6702
+ logger?.info?.("[platform-sso] sys_oauth_application row created", { environmentId, clientId });
6703
+ } catch (err) {
6704
+ logger?.warn?.("[platform-sso] sys_oauth_application create failed", {
6705
+ environmentId,
6706
+ error: err?.message
6707
+ });
6708
+ if (throwOnError) throw err;
6709
+ }
6710
+ return;
6711
+ }
6712
+ let currentRedirects = [];
6713
+ try {
6714
+ const raw = existing.redirect_uris;
6715
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
6716
+ if (Array.isArray(parsed)) currentRedirects = parsed.filter((s) => typeof s === "string");
6717
+ } catch {
6718
+ }
6719
+ const mergedRedirects = desiredRedirect && !currentRedirects.includes(desiredRedirect) ? [...currentRedirects, desiredRedirect] : currentRedirects;
6720
+ const repairPatch = {
6721
+ name: existing.name || `Project ${environmentId}`,
6722
+ client_secret: clientSecretStored,
6723
+ type: existing.type || "web",
6724
+ redirect_uris: JSON.stringify(mergedRedirects),
6725
+ grant_types: JSON.stringify(["authorization_code", "refresh_token"]),
6726
+ response_types: JSON.stringify(["code"]),
6727
+ scopes: JSON.stringify(["openid", "email", "profile"]),
6728
+ token_endpoint_auth_method: "client_secret_basic",
6729
+ require_pkce: false,
6730
+ skip_consent: true,
6731
+ disabled: false,
6732
+ subject_type: "public",
6733
+ updated_at: nowIso
6734
+ };
6735
+ try {
6736
+ await ql.update(
6737
+ "sys_oauth_application",
6738
+ repairPatch,
6739
+ { where: { id: existing.id } },
6740
+ { context: { isSystem: true } }
6741
+ );
6742
+ logger?.info?.("[platform-sso] sys_oauth_application repaired", {
6743
+ environmentId,
6744
+ clientId,
6745
+ redirect_uris: mergedRedirects
6746
+ });
6747
+ } catch (err) {
6748
+ logger?.warn?.("[platform-sso] sys_oauth_application repair failed", {
6749
+ environmentId,
6750
+ error: err?.message
6751
+ });
6752
+ if (throwOnError) throw err;
6753
+ }
6754
+ }
6755
+ async function backfillPlatformSsoClients(opts) {
6756
+ const { ql, baseSecret, logger, limit = 1e3 } = opts;
6757
+ if (!baseSecret) {
6758
+ logger?.warn?.("[platform-sso] backfill skipped \u2014 OS_AUTH_SECRET not set");
6759
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [] };
6760
+ }
6761
+ let projects = [];
6762
+ try {
6763
+ const rows = await ql.find("sys_environment", {
6764
+ limit,
6765
+ fields: ["id", "hostname", "status"]
6766
+ }, { context: { isSystem: true } });
6767
+ projects = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
6768
+ } catch (err) {
6769
+ logger?.warn?.("[platform-sso] backfill: sys_environment read failed", {
6770
+ error: err?.message
6771
+ });
6772
+ return { scanned: 0, seeded: 0, alreadyExisted: 0, failures: [{ environmentId: "<scan>", error: err?.message ?? String(err) }] };
6773
+ }
6774
+ let seeded = 0;
6775
+ let alreadyExisted = 0;
6776
+ const failures = [];
6777
+ for (const p of projects) {
6778
+ if (!p?.id) continue;
6779
+ const before = await (async () => {
6780
+ try {
6781
+ const r = await ql.find("sys_oauth_application", {
6782
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6783
+ limit: 1
6784
+ }, { context: { isSystem: true } });
6785
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6786
+ return list[0] ?? null;
6787
+ } catch {
6788
+ return null;
6789
+ }
6790
+ })();
6791
+ try {
6792
+ await seedPlatformSsoClient({ ql, environmentId: p.id, hostname: p.hostname, baseSecret, logger, throwOnError: true });
6793
+ if (before) alreadyExisted++;
6794
+ else {
6795
+ const after = await (async () => {
6796
+ try {
6797
+ const r = await ql.find("sys_oauth_application", {
6798
+ where: { client_id: derivePlatformSsoClientId(p.id) },
6799
+ limit: 1
6800
+ }, { context: { isSystem: true } });
6801
+ const list = Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : [];
6802
+ return list[0] ?? null;
6803
+ } catch (err) {
6804
+ return { _readErr: err?.message };
6805
+ }
6806
+ })();
6807
+ if (after && !after._readErr) seeded++;
6808
+ else failures.push({ environmentId: p.id, error: `post-insert read returned ${after ? JSON.stringify(after) : "null"}` });
6809
+ }
6810
+ } catch (err) {
6811
+ failures.push({ environmentId: p.id, error: err?.message ?? String(err) });
6812
+ }
6813
+ }
6814
+ logger?.info?.("[platform-sso] backfill complete", { scanned: projects.length, seeded, alreadyExisted, failures: failures.length });
6815
+ return { scanned: projects.length, seeded, alreadyExisted, failures };
6816
+ }
6817
+
8002
6818
  // src/cloud/artifact-kernel-factory.ts
8003
- init_platform_sso();
8004
6819
  function deriveProjectAuthSecret(baseSecret, environmentId) {
8005
6820
  return (0, import_node_crypto2.createHmac)("sha256", baseSecret).update(`project:${environmentId}`).digest("hex");
8006
6821
  }
@@ -8010,7 +6825,7 @@ var ArtifactKernelFactory = class {
8010
6825
  this.envRegistry = config.envRegistry;
8011
6826
  this.logger = config.logger ?? console;
8012
6827
  this.kernelConfig = config.kernelConfig;
8013
- this.authBaseSecret = (config.authBaseSecret ?? (0, import_types4.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
6828
+ this.authBaseSecret = (config.authBaseSecret ?? (0, import_types3.readEnvWithDeprecation)("OS_AUTH_SECRET", ["AUTH_SECRET", "BETTER_AUTH_SECRET"]) ?? "").trim();
8014
6829
  }
8015
6830
  async create(environmentId) {
8016
6831
  let cached = this.envRegistry.peekById(environmentId);
@@ -8123,7 +6938,7 @@ var ArtifactKernelFactory = class {
8123
6938
  this.logger.warn?.("[ArtifactKernelFactory] OS_AUTH_SECRET not set \u2014 per-project AuthPlugin skipped (auth endpoints will return 404)", { environmentId });
8124
6939
  }
8125
6940
  try {
8126
- const multiTenant = String((0, import_types4.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
6941
+ const multiTenant = String((0, import_types3.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8127
6942
  if (multiTenant) {
8128
6943
  try {
8129
6944
  const { OrgScopingPlugin } = await import("@objectstack/plugin-org-scoping");
@@ -9326,7 +8141,7 @@ async function createObjectOSStack(config) {
9326
8141
  // src/cloud/marketplace-install-local-plugin.ts
9327
8142
  var import_node_fs4 = require("fs");
9328
8143
  var import_node_path7 = require("path");
9329
- var import_types5 = require("@objectstack/types");
8144
+ var import_types4 = require("@objectstack/types");
9330
8145
  var ROUTE_BASE = "/api/v1/marketplace/install-local";
9331
8146
  var DEFAULT_DIR = ".objectstack/installed-packages";
9332
8147
  function safeFilename(manifestId) {
@@ -9870,7 +8685,7 @@ var MarketplaceInstallLocalPlugin = class {
9870
8685
  }
9871
8686
  }
9872
8687
  if (opts.seedNow && datasets.length > 0) {
9873
- const multiTenant = String((0, import_types5.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
8688
+ const multiTenant = String((0, import_types4.readEnvWithDeprecation)("OS_MULTI_ORG_ENABLED", "OS_MULTI_TENANT") ?? "false").toLowerCase() !== "false";
9874
8689
  try {
9875
8690
  const ql = ctx.getService("objectql");
9876
8691
  let metadata;
@@ -9988,9 +8803,6 @@ var MarketplaceInstallLocalPlugin = class {
9988
8803
  }
9989
8804
  };
9990
8805
 
9991
- // src/index.ts
9992
- init_platform_sso();
9993
-
9994
8806
  // src/sandbox/script-runner.ts
9995
8807
  var UnimplementedScriptRunner = class {
9996
8808
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -10016,7 +8828,7 @@ init_body_runner();
10016
8828
  // src/index.ts
10017
8829
  var import_rest = require("@objectstack/rest");
10018
8830
  __reExport(index_exports, require("@objectstack/core"), module.exports);
10019
- var import_types6 = require("@objectstack/types");
8831
+ var import_types5 = require("@objectstack/types");
10020
8832
  // Annotate the CommonJS export names for ESM import in node:
10021
8833
  0 && (module.exports = {
10022
8834
  AppPlugin,
@@ -10027,6 +8839,7 @@ var import_types6 = require("@objectstack/types");
10027
8839
  DEFAULT_CLOUD_URL,
10028
8840
  DEFAULT_RATE_LIMITS,
10029
8841
  DriverPlugin,
8842
+ ExternalValidationPlugin,
10030
8843
  FileArtifactApiClient,
10031
8844
  HttpDispatcher,
10032
8845
  HttpServer,
@@ -10065,6 +8878,7 @@ var import_types6 = require("@objectstack/types");
10065
8878
  collectBundleHooks,
10066
8879
  createDefaultHostConfig,
10067
8880
  createDispatcherPlugin,
8881
+ createExternalValidationPlugin,
10068
8882
  createObjectOSStack,
10069
8883
  createRestApiPlugin,
10070
8884
  createStandaloneStack,